我们昨天分析完了parse
的内容,今天我们来分析compile
的内容。
坏蛋Dan:vue/compiler-core源码分析学习--day2: parse部分
本来不打算拆开来的。。。没想到字数上限了我还没分析完三分之二。。。。
所以这里拆分为两部分:
一、
二、
nodeTransforms
中注册的插件genCode
生成render function
过程中的所有类型今天我们来分析第一部分
由于这个compile
方法贯穿三个包:compiler-sfc
、compiler-dom
以及今天分析的compiler-core
三个包,所以这里需要把两外两处涉及到的地方都放出来,先把入口说明白了,到时候分析也能清晰点。
先说下这三者的关系,
compiler-dom
包中的compile
方法基于compiler-core
包中baseCompile
方法封装了一层compiler-sfc
方法调用compiler-dom
包中的compile
方法,传入自己的参数。baseCompile
是我们要分析的入口,所以这里就不放出来了。
我们先来看下compiler-dom
中调用这个方法传入的options
。
在compiler-dom/src/index.ts
文件中。
可以看到compile-dom
中对compiler-core/baseCompile
方法做了一层封装,然后加入了一些加工函数,具体分析可以看之前的文章。
vue/compiler-dom源码分析学习--final:整理
然后我们来看下compiler-sfc
里面的调用,在compiler-sfc/src/compileTemplate.ts
文件中
传入一些参数。
对这一块分析感兴趣的可以看下之前的文章
我们来看下最终组合的参数
baseCompile
开始分析前,推荐下:Vue Template Explorer
可以直观的看到render function
,可以辅助你阅读源码.
另外,这里最好记下哪个指令搭配哪个辅助函数,哪种节点搭配哪种辅助函数,这对后面我们学习runtime
相关的包是很有帮助的。
补充:block
的概念[1
这后面我们会遇到一些block
的概念,避免不理解先放这里了
prefixIdentifiers
:这玩意儿应该是用于cache
的,目前暂时还不清楚是做什么用的。ast
自然就是通过baseParse
方法解析出来的template AST
。从这里也可以看出来它俩的分工了,parse
专门用来转化成ast
,而compile
具体做了什么我们来慢慢分析。getBaseTransformPreset
:老规矩,这样不会有太多代码堆积在主流程分析上。然后对typescript
的场景做支持处理,如果检测到使用ts
但是没有ts
插件,这个时候就会自动加上。
接着调用transform
方法,把ast
传入里面。
最后调用generate
方法将ast
处理并返回。
注意这里的transform
和generate
不是babel
的transform
和generate
,切忌不要搞混,这个用法太像了。。。
transform
createTransformContext
:用来创建转换执行上下文的,具体代码看主流程transform中遇到的函数
里面的分析traverseNode
:同上,这里简单的说就是遍历节点,给每个节点都执行一遍nodeTranforms
里面注册的回调,并执行这些回调返回的回调。hoistStatic
:同上,这里简单的说就是查找能否静态提升的节点并提升,甚至是stringfy
。createRootCodegen
:同上,简单的说就是子节点们的codegenNode
都已经好了,那么就轮到根节点了。。最后就是将这些过程中存储的数据赋值给root
,也就是compile.ts
文件中传入的AST
根节点。
generate
顾名思义就是将之前的codeGen
节点转换为render function
。
createCodegenContext
:生成code generate
执行上下文,代码就不看了,里面有些方法我们遇到了再细嗦。onContextCreated
:创建上下文时需要执行的回调,这里就不说了,我们并没有。preamble
:这个词翻译过来叫“前言”,在这里应该是在做准备,同样的,涉及到的两个函数的代码分析放到下面genModulePreamble
:这个方法简单的说就是import
和生成hoists
的const
常量,具体分析请往下看。genFunctionPreamble
:这个函数我们就不看了,我们这里没有涉及到,感兴趣的大佬可自行查看。genAssets
:老规矩,简单的说就是转换成一个辅助函数,runtime
时引入处理component/filter/directive
。 filter
和directive
的我们就不看了,不过注意这个directive
是自定义directive
[2 。ast.temps
:这个是临时变量,我们这里没遇到,不得已需要缓存到全局deindent
:自然就是换行 + 减少缩进。genNode
:老规矩,简单的说就是给每一个node
都创建runtime
函数,然后一个套一个。那么compile
的就都说完了。
流程比较清晰,你可以参考babel
的core
来了解:
transform
: 里面对每个节点都做了plugin
里的操作,然后对部分节点hoistStatic
以及staticStringify
化,然后给每个节点都创建一个codegenNode
字段,它们将被用于generate
中转换成render function
。generate
: 顾名思义,就是生成render function
,通过transform
加工(这里说加工是因为解析是在parse
阶段)之后拿到的codegenNode
通过runtime
辅助函数包裹转换成可被runtime
直接使用 的function
字符串。最后来看下数据
源码:
render function
(mode == module
):
可读性极差~,不过线上要啥可读性呢~
mark
下每个节点类型对应什么样子的源码,这对于后面学习runtime
来说是很有帮助的,我这里没有罗列出来,后面有空再说。不过你也可以边学runtime
包边用开头推荐的页面看下每种源码对应的节点类型,也是一个不错的选择NodeTypes
实际上包含的节点类型可以大致分类两类,一类是AST
的节点类型,另一类是AST
的codegenNode
的节点类型,所以codegenNode
的类型只会出现在traverseNode
之后。transform
中遇到的函数这个方法没什么好说的,就是预先整合需要用于转换的辅助函数(这里的辅助不是runtime
的辅助)。
至于这里面的辅助函数我们先不在这里分析,需要用到的时候单独给它们开个小标题分析。
量有些大,我们来说下方法即可。
helper
:用来存储辅助函数的名字,这里的辅助函数就是runtime
的辅助函数removeHelper
:自然就是移除对应的辅助函数,不过由于这个执行上下文是全局唯一的,所以同名的辅助函数很大概率是存在多个的,所以这里为了确保辅助函数的存取准确性,用了个数来计算,当没有辅助函数之后就移除这个辅助函数,否则减一。helperString
:将辅助函数stringfy
化,这样做是为了runtime
可以直接使用。replaceNode
:用来替换node
节点的。removeNode
:移除节点,将这个节点赋值为null
,然后调用上下文的onNodeRemoved
方法。然后将它移除。onNodeRemoved
:目前是初始化状态,没有可执行代码addIdentifiers
:新增一个节点,调用addId
这个方法。removeIdentifiers
:自然就是移除节点。hoist
:将节点标记为CAN_HOIST
。到时候会被静态提升。cache
:缓存处理。这些应该是提供给插件们使用的方法,插件有可能将当前节点删除或者新插入一个节点等操作,所以这里提供了这些方法。依旧是和babel
类似。
具体内容到时具体地方使用到会细嗦。
其实看到这里大概能猜到整体是怎么样的了,应该是惨遭babel
的core
[3包的架构来写的,包括这里traversed
以及插件的注册。扯远了。
这里开始处理之前注册了的nodeTransforms
插件,插件们是啥,做了什么这里先不分析,等会会单独开个一级标题分析它们。
这一块代码很简单,调用nodeTransforms
注册的回调们,然后将它们执行,注意,每一个节点都会调用所有这些回调。
如果当前节点在这些回调的其中一个中被remove
了,那么直接就return
处理。
如果没有,那么再重新赋值一遍context.currentNode
,确保这个node
没有替换掉。
然后就是针对不同的节点类型进行处理:
COMMENT
:也就是注释节点,对应的辅助函数是createCommentVNode
INTERPOLATION
:mustache
也就是双大括号语法,昨天parse
部分分析过的,这里就不多说了,对应的辅助函数是toDisplayString
IF
:会将每一个分支都递归调用traverseNode
方法来处理子节点IF_BRANCH
:同下FOR
:同下ELEMENT
:同下ROOT
:调用traverseChildren
这个方法.注意这里的context.onNodeRemoved
,上面我们初始化的时候是一个空函数,而这里被替换成i--
的函数,所以context
上的一些数据是会在执行的过程中被替换的,需要注意。
结束遍历后,将node
赋值给context.currentNode
,这里为啥又要重新将context.currentNode
赋值为node
呢?我们暂时不清楚是为啥,不过可以猜测,由于currentNode
是全局的,所以这里应该会在遍历/递归的过程中发生变化,所以遍历/递归结束之后再替换回当前的节点,避免节点不对导致后续一些列问题。
如果这些注册的回调有返回回调,那么顺便帮它们执行了,注意,这里是i--
来执行的,也就是说是逆序,这里类似调用栈,后进先出。
在分析前,我们先需要知道静态提升是啥东西,我之前分析compiler-dom
的时候有说到过,感兴趣的可以去看下,这里就不再解释了。
vue/compiler-dom源码分析学习--day4: 字符串化hoist节点
另外官网也有相应的解释:
Rendering Mechanism | Vue.js (vuejs.org)
isSingleElementRoot
方法就不看了,就是判断你这个文件中是否存在两个根节点。walk
:来看下代码代码较长,我们一点一点的分析。
其实我们之前分析compiler-dom
这个包中的staticStringfy
的时候也是遇到过了一个walk
,但实际上俩walk
并没有关系。
开头一句注释:只有文本和纯元素可以静态提升。
getConstantType
:这个方法这里就不分析了,放到下面的章节中。简单的说就是在判断节点是否可以静态提升。ConstantTypes
是一个枚举,有四种变体,这个之前其实有说过,不过这里再说一次加深印象。其中变体等级越高越安静稳定。那么是用在什么时候的呢?用在patch
也就是打补丁的时候,更准确一点,是diff
阶段。
NOT_CONSTANT
:一定要处理,不能跳过,也不能静态提升,更不能字符串化。CAN_SKIP_PATCH
:打补丁的时候可以绕过。CAN_HOIST
: 可以静态提升。CAN_STRINGFY
:可以字符串化。而patchFlags
[4 , 是用来标记当前节点的打补丁类型,由于这个类型的唯一性,在打补丁阶段可以快速定位并且做最少的操作。
可以简单的看下
这里面还使用了位运算符。你可以看下注释里的介绍。
这里还用到了hoist
这个方法,由于代码上面createTransformContext
方法中有贴出来了,所以这里只是细讲一下,忘了代码的可以回看。
hoist
hoist
这个方法中,如果表达式是一段字符串(在hoistStatic
中确实是一个字符串),那就先处理成一个单表达式节点,然后将它存入context.hoists
中。
接着再根据这个节点在context.hoists
这个数组中的下标位置创建一个新的单表达式节点,content
自然就是_hoisted_${index}
,注意这里isStatic
是false
,有点没搞懂,先mark
下来。
然后将旧的单节点表达式作为新的单节点表达式中的hoisted
字段的值。
最后返回。
然后回到我们的walk
方法中。
getPatchFlag
: 代码就不看了,就是在获取这个节点的patchFlag
。getGeneratedPropsConstantType
:这个方法我也放到下面开个小标题,这里简单的说就是判断这个节点的属性们是否都可以hoist/stringify
,感觉都没必要把代码贴出来了。。。。。dynamicProps
:这个我们又不认识。。mark
下。scopes.vSlot
:有几种情况会生成scope
,比如slot/for
等。transformHoist
:这个就是compiler-dom
的staticStringify
,之前分析过了,这里就不多说了。那么来总结下这个方法:简单的说这个方法就是在查找哪些节点(包括属性节点)可以hoist/stringify
,然后将它们hoist/stringify
处理, 注意这里改动都是改动node.codegenNode
而不是node
本身了,因为这个阶段是在traverseNode
之后。
这里再说下三种特殊场景需要递归的:
component
,这个时候自然需要递归处理这个节点的子组件并且可以确定这些都是slot
的内容v-for
,除了只有一个子节点的情况,其它都是得递归继续判断的。v-if
,同上。另外如果这个节点的所有子节点都可以hoist
甚至stringify
,那这个节点自身自然可以hoist/stringify
。
这里你应该有个疑问,那就是第一个图为什么只是hoist
,而不是stringify
。
其实这个点在分析staticStringify
的时候说过,由于这么做的性能体现是需要超出多少个节点的时候才能体现出来的,所以少于某个阈值的时候是不会stringify
的。
我们来改下第一张图里的源码,当节点变多了之后就变成stringify
了。
补充:Rendering Mechanism | Vue.js (vuejs.org)
有标记的节点才会被跟踪,这样打补丁的时候就能省下很多时间,前面有的地方应该说错了。。。
子节点们的codegenNode
都已经好了,那么就轮到根节点了。
这里有两种情况:
vue3.x
支持的新写法,允许template
中存在多个根节点。多根节点使用Fragement
,当然,它也是一个block
,block
的概念和作用我们开篇就讲了,这里就不多说了。block
,如果根节点本身就是个block
,比如v-for/v-if/
节点,它们自身就是block
了,所以这里会把根节点的codegenNode
替换成子节点而不是创建一个block
makeBlock
这个方法就不看了,就是移除createElementVNode/createVNode
的辅助函数,增加openBlock
和createBlock/createElementBlock
这俩辅助函数createVNodeCall
:这个方法也不看了,就是创建一个VNODE_CALL
的节点这个函数看名字应该是用来判断属性是否可以hoist
或者stringify
的。
注意这里的props
,它们已经是被traverseNode
里的nodeTransforms
里注册的插件们加工过的了。
JS_OBJECT_EXPRESSION
:是codegen
之后的类型,先mark
下,后面分析插件的时候应该会遇到。key
:属性的名字value
:属性的值getConstantType
:这个下面会说, 这里简单的说就是判断这个节点的每一处地方是否都可以hoist/stringify
。getConstantTypeOfHelperCall
:这个函数就不看代码了,获取辅助函数调用的类型,有些是可以被hoist
的。这个方法挺简单的,就是判断这个节点的属性和属性的值是否可以被hoist
或者stringify
。两者取等级最低的那个。
其实这个方法之前分析compiler-dom
的时候就遇到了,那时候没有说,只是看了某个node
类型的处理过程,现在算是把坑埋上了。
codegenNode
是什么时候放到node
上面的呢?实际上是在一个插件上postTransformElement
方法上,这个我们会在分析指令/插件的时候分析到。当NodeType
是ELELMENT
的时候,如果tag
的类型不是ELEMENT
,那就是NOT_CONSTANT
的,为什么呢?还记得之前说的么,ElementTypes
指的是vDom
的类型,除了ELEMENT
之外就是COMPONENT/SLOT/TEMPLATE
这仨货,所以自然都是NOT_CONSTANT
的。
VNODE_CALL
这个类型我们后面会遇到isBlock
先mark
下,也是到时候分析插件的时候会遇到,应该是和svg/foreignObject
有关的.对于没有flag
的元素节点,它们的属性中可能引用了奇怪的东西,比如scope
变量,并且这个变量表达式是不能hoist
的,所以每次都需要check
一遍节点的属性才行。既然属性不能放过,那它的子节点们自然更不可能错过。另外这里再强调一次,遇到等级低的就减低等级,宁可杀错不可放过。
接着又遇到isBlock
了,看到了句注释only svg/foreignObject could be block here
,这么说前面猜测和svg/foreignObject
有关是正确的。
遇到svg
,isBlock
字段会变成true
,这里还做了removeHelper
的操作,处理完之后isBlock
字段又置为false
了。然后又新增一个helper
,注意这里的两个helper
,是不一样的。这里可以猜测下是因为前面处理svg
有些问题,并不能处理成VNode
,所以需要放到这里
这里有个else
逻辑:element
节点自身是没有flag
的,如果有那肯定是NOT_CONSTANT
等级的,这里先mark
下来,应该后面会遇到。
这里还剩下几类:
TEXT/COMMENT
:这俩货自然是CAN_STRINGIFY
最高规格对待IF/FOR/IF_BRANCH
:这仨也不用多想,自然的是NOT_CONSTANT
最低规格对待。INTERPOLATION/TEXT_CALL
:这个INTERPOLATION
我们已经知道了,是{{}}
相关的,但是这个TEXT_CALL
,我们目前还没遇到过,暂时还不清楚是什么场景,所以先mark
下,他俩因为是复合类型的,所以需要确定他们的content
才行。SIMPLE_EXPRESSION
:这个比较简单,在hoist
之前就可以确定下来了,我们在compiler-dom
的时候接触过,所以处理的位置应该是在nodeTransforms
或者directiveTransforms
里面,这里就不多说了。COMPOUND_EXPRESSION
:复合表达式,存在多个表达式,这个自然是给每一个表达式过一遍。NOT_COSTANT
:自然是宁杀错不放过。稍微总结下这个方法:简单的说就是给几种节点类型做判断,判断是否可以hoist/stringify
。
generate
中遇到的函数PUSH_SCOPED_ID
:runtime
的辅助函数,pushScopeId
POP_SCOPE_ID
:同上,popScopeId
,这俩目前猜测应该是用来生成scopeId
相关的,具体等到时我们去分析runtime
的包的时候再看。optimizeImports
:应该是用来兼容webpack
的code-split
也就是代码切割的,由于webpack
会把import
的函数装换成Object(a.b)
或者(0, a.b)
,这样可能有潜在的性能开销以及负载变大的问题。所以这里把导入的函数分配给变量而不是直接使用,这样就能避免上面的问题,每个组件多开销大概50b
左右。ast.imports
:如果你看过我之前分析compiler-sfc
的compileTemplate
的话,应该知道这个是干啥的,当然,看了也大概率忘了,我自己都忘了,看数据才想起来的。当时分析compileTemplate
方法的时候我顺便把几个nodeTransforms
注册的回调也跟着分析了。这个是和src
等属性相关的,因为它们有可能用的是本地资源连接又或者是webpack
特殊用法require
包裹的路径等,所以这里需要重写路径等处理。
genImports
:代码很少,我们直接看genNode
方法是根据NodeTypes
的类型来判断具体执行哪个方法,所以这里就不看了,这里的NodeTypes
是4
,对应SIMPLE_EXPRESSION
,所以这里直接拿过来了。
newline
:这里说一下,就是换行以及+/-
缩进。这里用到了context
的push
方法,我们顺便来细嗦下push
这个方法。
直接字符串拼接,然后(在这里)content
也就是_imports_index
是带有_ctx.
的,那么就去掉。
addMapping
就不看了,和代码位置相关的。advancePositionWithMutation
这个方法之前有说过,这里就不说了,在这里也是用来改变定位的,因为我们push
了code
,所以我们也需要迁移code.length
个位置,这样定位才能准确。最后会变成这样\nimport _imports_0 from './a.jpg'\nimport _imports_1 from './b.jpg'\n'
。
回到genModulePreamble
方法中
genHoists
:这里代码也比较少,我们直接看就好看名字就知道是用来生成静态提升的节点的runtime
代码的。
注意这里面的节点前面有分析过了,不仅有dom
节点,还有属性节点。
genScopeId
:这个是生成scopeId
的辅助函数,这里会变成const _withScopeId = n => (push_scope_id(), n = n(), pop_scope_id(), n)\n
其它没啥好说的,最后的数据类似下面这样
至于genCode
里面做了什么,这里就不分析了,后面再开个一级标题一个个分析。
回到genModulePreamble
方法中
最后还多加了个export
,为什么呢?因为已经import
和常量声明都已经完毕了,可以准备整合导出render function
了。
这个方法其实没什么好说的,就是在组装一个辅助函数。有三种类型component/filter/directive
。我们这里的场景是component
,对应的runtime
函数是resolveComponent
。
另外把这个代码贴出来最主要的原因就是这里有一段对自引用的分析。
在vue
组件中,我们可以在template
通过使用当前组件的名字来使用当前组件达成递归组件的方式。这里通过注释可以了解到是怎么判断的:potential component implicit self-reference inferred from SFC filename
,实际上是通过文件名推断出来的。并且在转换过程中会给它的结尾加上__self
用于区分。
来看下转换出来的runtime
代码: const _component_Add = _resolveComponent("Add")
;
另外两种场景就不看了,类似的。
祝大家新年快乐~~~~~~~~
编辑于 2023-01-21 18:19・IP 属地广东