坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

我们昨天分析完了parse的内容,今天我们来分析compile的内容。

坏蛋Dan:vue/compiler-core源码分析学习--day2: parse部分

本来不打算拆开来的。。。没想到字数上限了我还没分析完三分之二。。。。

所以这里拆分为两部分:

一、

  1. 主流程
  2. 主流程中遇到的一些函数

二、

  1. nodeTransforms中注册的插件
  2. 分析genCode生成render function过程中的所有类型

今天我们来分析第一部分


入口参数

由于这个compile方法贯穿三个包:compiler-sfccompiler-dom以及今天分析的compiler-core三个包,所以这里需要把两外两处涉及到的地方都放出来,先把入口说明白了,到时候分析也能清晰点。

先说下这三者的关系,

  1. compiler-dom包中的compile方法基于compiler-core包中baseCompile方法封装了一层
  2. 然后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文件中

传入一些参数。

对这一块分析感兴趣的可以看下之前的文章

vue/compiler-sfc源码分析学习--汇总

我们来看下最终组合的参数


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处理并返回。

注意这里的transformgenerate不是babeltransformgenerate,切忌不要搞混,这个用法太像了。。。

transform

  • createTransformContext:用来创建转换执行上下文的,具体代码看主流程transform中遇到的函数里面的分析
  • traverseNode:同上,这里简单的说就是遍历节点,给每个节点都执行一遍nodeTranforms里面注册的回调,并执行这些回调返回的回调。
  • hoistStatic:同上,这里简单的说就是查找能否静态提升的节点并提升,甚至是stringfy
  • createRootCodegen:同上,简单的说就是子节点们的codegenNode都已经好了,那么就轮到根节点了。。

最后就是将这些过程中存储的数据赋值给root,也就是compile.ts文件中传入的AST根节点。


generate

generate顾名思义就是将之前的codeGen节点转换为render function

  • createCodegenContext:生成code generate执行上下文,代码就不看了,里面有些方法我们遇到了再细嗦。

  • onContextCreated:创建上下文时需要执行的回调,这里就不说了,我们并没有。
  • preamble:这个词翻译过来叫“前言”,在这里应该是在做准备,同样的,涉及到的两个函数的代码分析放到下面
  • genModulePreamble:这个方法简单的说就是import和生成hoistsconst常量,具体分析请往下看。
  • genFunctionPreamble:这个函数我们就不看了,我们这里没有涉及到,感兴趣的大佬可自行查看。
  • genAssets:老规矩,简单的说就是转换成一个辅助函数,runtime时引入处理component/filter/directivefilterdirective的我们就不看了,不过注意这个directive是自定义directive[2
  • ast.temps:这个是临时变量,我们这里没遇到,不得已需要缓存到全局
  • deindent:自然就是换行 + 减少缩进。
  • genNode:老规矩,简单的说就是给每一个node都创建runtime函数,然后一个套一个。

总结

那么compile的就都说完了。

流程比较清晰,你可以参考babelcore来了解:

  1. transform: 里面对每个节点都做了plugin里的操作,然后对部分节点hoistStatic以及staticStringify化,然后给每个节点都创建一个codegenNode字段,它们将被用于generate中转换成render function
  2. generate: 顾名思义,就是生成render function,通过transform加工(这里说加工是因为解析是在parse阶段)之后拿到的codegenNode通过runtime辅助函数包裹转换成可被runtime直接使用 的function字符串。

最后来看下数据

源码:

render function(mode == module):

可读性极差~,不过线上要啥可读性呢~

补充

  1. 你可以mark下每个节点类型对应什么样子的源码,这对于后面学习runtime来说是很有帮助的,我这里没有罗列出来,后面有空再说。不过你也可以边学runtime包边用开头推荐的页面看下每种源码对应的节点类型,也是一个不错的选择
  2. 关于节点类型这里还有些要说的,NodeTypes实际上包含的节点类型可以大致分类两类,一类是AST的节点类型,另一类是ASTcodegenNode的节点类型,所以codegenNode的类型只会出现在traverseNode之后。
  3. 为了方便理解,这里画了张图


主流程transform中遇到的函数

getBaseTransformPreset

这个方法没什么好说的,就是预先整合需要用于转换的辅助函数(这里的辅助不是runtime的辅助)。

至于这里面的辅助函数我们先不在这里分析,需要用到的时候单独给它们开个小标题分析。


createTransformContext

量有些大,我们来说下方法即可。

  • helper:用来存储辅助函数的名字,这里的辅助函数就是runtime的辅助函数
  • removeHelper:自然就是移除对应的辅助函数,不过由于这个执行上下文是全局唯一的,所以同名的辅助函数很大概率是存在多个的,所以这里为了确保辅助函数的存取准确性,用了个数来计算,当没有辅助函数之后就移除这个辅助函数,否则减一。
  • helperString:将辅助函数stringfy化,这样做是为了runtime可以直接使用。
  • replaceNode:用来替换node节点的。
  • removeNode:移除节点,将这个节点赋值为null,然后调用上下文的onNodeRemoved方法。然后将它移除。
  • onNodeRemoved:目前是初始化状态,没有可执行代码
  • addIdentifiers:新增一个节点,调用addId这个方法。
  • removeIdentifiers:自然就是移除节点。
  • hoist:将节点标记为CAN_HOIST。到时候会被静态提升。
  • cache:缓存处理。

这些应该是提供给插件们使用的方法,插件有可能将当前节点删除或者新插入一个节点等操作,所以这里提供了这些方法。依旧是和babel类似。

具体内容到时具体地方使用到会细嗦。


traverseNode

其实看到这里大概能猜到整体是怎么样的了,应该是惨遭babelcore[3包的架构来写的,包括这里traversed以及插件的注册。扯远了。

这里开始处理之前注册了的nodeTransforms插件,插件们是啥,做了什么这里先不分析,等会会单独开个一级标题分析它们。

这一块代码很简单,调用nodeTransforms注册的回调们,然后将它们执行,注意,每一个节点都会调用所有这些回调。

如果当前节点在这些回调的其中一个中被remove了,那么直接就return处理。

如果没有,那么再重新赋值一遍context.currentNode,确保这个node没有替换掉。

然后就是针对不同的节点类型进行处理:

  • COMMENT:也就是注释节点,对应的辅助函数是createCommentVNode
  • INTERPOLATIONmustache也就是双大括号语法,昨天parse部分分析过的,这里就不多说了,对应的辅助函数是toDisplayString
  • IF:会将每一个分支都递归调用traverseNode方法来处理子节点
  • IF_BRANCH:同下
  • FOR:同下
  • ELEMENT:同下
  • ROOT:调用traverseChildren这个方法.

注意这里的context.onNodeRemoved,上面我们初始化的时候是一个空函数,而这里被替换成i--的函数,所以context上的一些数据是会在执行的过程中被替换的,需要注意。

结束遍历后,将node赋值给context.currentNode,这里为啥又要重新将context.currentNode赋值为node呢?我们暂时不清楚是为啥,不过可以猜测,由于currentNode是全局的,所以这里应该会在遍历/递归的过程中发生变化,所以遍历/递归结束之后再替换回当前的节点,避免节点不对导致后续一些列问题。

如果这些注册的回调有返回回调,那么顺便帮它们执行了,注意,这里是i--来执行的,也就是说是逆序,这里类似调用栈,后进先出。


hoistStatic

在分析前,我们先需要知道静态提升是啥东西,我之前分析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},注意这里isStaticfalse,有点没搞懂,先mark下来。

然后将旧的单节点表达式作为新的单节点表达式中的hoisted字段的值。

最后返回。

然后回到我们的walk方法中。

  • getPatchFlag: 代码就不看了,就是在获取这个节点的patchFlag
  • getGeneratedPropsConstantType:这个方法我也放到下面开个小标题,这里简单的说就是判断这个节点的属性们是否都可以hoist/stringify,感觉都没必要把代码贴出来了。。。。。
  • dynamicProps:这个我们又不认识。。mark下。
  • scopes.vSlot:有几种情况会生成scope,比如slot/for等。
  • transformHoist:这个就是compiler-domstaticStringify,之前分析过了,这里就不多说了。

那么来总结下这个方法:简单的说这个方法就是在查找哪些节点(包括属性节点)可以hoist/stringify,然后将它们hoist/stringify处理, 注意这里改动都是改动node.codegenNode而不是node本身了,因为这个阶段是在traverseNode之后。

这里再说下三种特殊场景需要递归的:

  1. 节点是一个component,这个时候自然需要递归处理这个节点的子组件并且可以确定这些都是slot的内容
  2. v-for,除了只有一个子节点的情况,其它都是得递归继续判断的。
  3. v-if,同上。

另外如果这个节点的所有子节点都可以hoist甚至stringify,那这个节点自身自然可以hoist/stringify

这里你应该有个疑问,那就是第一个图为什么只是hoist,而不是stringify

其实这个点在分析staticStringify的时候说过,由于这么做的性能体现是需要超出多少个节点的时候才能体现出来的,所以少于某个阈值的时候是不会stringify的。

我们来改下第一张图里的源码,当节点变多了之后就变成stringify了。

补充:Rendering Mechanism | Vue.js (vuejs.org)

有标记的节点才会被跟踪,这样打补丁的时候就能省下很多时间,前面有的地方应该说错了。。。


createRootCodegen

子节点们的codegenNode都已经好了,那么就轮到根节点了。

这里有两种情况:

  • 多根节点:vue3.x支持的新写法,允许template中存在多个根节点。多根节点使用Fragement,当然,它也是一个blockblock的概念和作用我们开篇就讲了,这里就不多说了。
  • 单根节点:单根节点则是创建一个block,如果根节点本身就是个block,比如v-for/v-if/节点,它们自身就是block了,所以这里会把根节点的codegenNode替换成子节点而不是创建一个block
  • makeBlock这个方法就不看了,就是移除createElementVNode/createVNode的辅助函数,增加openBlockcreateBlock/createElementBlock这俩辅助函数
  • createVNodeCall:这个方法也不看了,就是创建一个VNODE_CALL的节点


getGeneratedPropsConstantType

这个函数看名字应该是用来判断属性是否可以hoist或者stringify的。

注意这里的props,它们已经是被traverseNode里的nodeTransforms里注册的插件们加工过的了。

  • JS_OBJECT_EXPRESSION:是codegen之后的类型,先mark下,后面分析插件的时候应该会遇到。
  • key:属性的名字
  • value:属性的值
  • getConstantType:这个下面会说, 这里简单的说就是判断这个节点的每一处地方是否都可以hoist/stringify
  • getConstantTypeOfHelperCall:这个函数就不看代码了,获取辅助函数调用的类型,有些是可以被hoist的。

这个方法挺简单的,就是判断这个节点的属性和属性的值是否可以被hoist或者stringify。两者取等级最低的那个。


getConstantType

其实这个方法之前分析compiler-dom的时候就遇到了,那时候没有说,只是看了某个node类型的处理过程,现在算是把坑埋上了。

  • codegenNode是什么时候放到node上面的呢?实际上是在一个插件上postTransformElement方法上,这个我们会在分析指令/插件的时候分析到。

NodeTypeELELMENT的时候,如果tag的类型不是ELEMENT,那就是NOT_CONSTANT的,为什么呢?还记得之前说的么,ElementTypes指的是vDom的类型,除了ELEMENT之外就是COMPONENT/SLOT/TEMPLATE这仨货,所以自然都是NOT_CONSTANT的。

  • VNODE_CALL这个类型我们后面会遇到
  • isBlockmark下,也是到时候分析插件的时候会遇到,应该是和svg/foreignObject有关的.

对于没有flag的元素节点,它们的属性中可能引用了奇怪的东西,比如scope变量,并且这个变量表达式是不能hoist的,所以每次都需要check一遍节点的属性才行。既然属性不能放过,那它的子节点们自然更不可能错过。另外这里再强调一次,遇到等级低的就减低等级,宁可杀错不可放过。

接着又遇到isBlock了,看到了句注释only svg/foreignObject could be block here,这么说前面猜测和svg/foreignObject有关是正确的。

遇到svgisBlock字段会变成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中遇到的函数

genModulePreamble

  • PUSH_SCOPED_IDruntime的辅助函数,pushScopeId
  • POP_SCOPE_ID:同上,popScopeId,这俩目前猜测应该是用来生成scopeId相关的,具体等到时我们去分析runtime的包的时候再看。
  • optimizeImports:应该是用来兼容webpackcode-split也就是代码切割的,由于webpack会把import的函数装换成Object(a.b)或者(0, a.b),这样可能有潜在的性能开销以及负载变大的问题。所以这里把导入的函数分配给变量而不是直接使用,这样就能避免上面的问题,每个组件多开销大概50b左右。
  • ast.imports:如果你看过我之前分析compiler-sfccompileTemplate的话,应该知道这个是干啥的,当然,看了也大概率忘了,我自己都忘了,看数据才想起来的。

当时分析compileTemplate方法的时候我顺便把几个nodeTransforms注册的回调也跟着分析了。这个是和src等属性相关的,因为它们有可能用的是本地资源连接又或者是webpack特殊用法require包裹的路径等,所以这里需要重写路径等处理。

  • genImports:代码很少,我们直接看

genNode方法是根据NodeTypes的类型来判断具体执行哪个方法,所以这里就不看了,这里的NodeTypes4,对应SIMPLE_EXPRESSION,所以这里直接拿过来了。

  • newline:这里说一下,就是换行以及+/-缩进。

这里用到了contextpush方法,我们顺便来细嗦下push这个方法。

直接字符串拼接,然后(在这里)content也就是_imports_index是带有_ctx.的,那么就去掉。

  • addMapping就不看了,和代码位置相关的。
  • advancePositionWithMutation这个方法之前有说过,这里就不说了,在这里也是用来改变定位的,因为我们pushcode,所以我们也需要迁移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了。


genAssets

这个方法其实没什么好说的,就是在组装一个辅助函数。有三种类型component/filter/directive。我们这里的场景是component,对应的runtime函数是resolveComponent

另外把这个代码贴出来最主要的原因就是这里有一段对自引用的分析。

vue组件中,我们可以在template通过使用当前组件的名字来使用当前组件达成递归组件的方式。这里通过注释可以了解到是怎么判断的:potential component implicit self-reference inferred from SFC filename,实际上是通过文件名推断出来的。并且在转换过程中会给它的结尾加上__self用于区分。

来看下转换出来的runtime代码: const _component_Add = _resolveComponent("Add")

另外两种场景就不看了,类似的。


最后

祝大家新年快乐~~~~~~~~

参考

  1. ^vue-tree-flattern https://vuejs.org/guide/extras/rendering-mechanism.html#tree-flattening
  2. ^vue-custom-directive https://vuejs.org/guide/reusability/custom-directives.html#custom-directives
  3. ^babel-core https://babeljs.io/docs/en/babel-core
  4. ^vue-patchFlags https://vuejs.org/guide/extras/rendering-mechanism.html#patch-flags

编辑于 2023-01-21 18:19・IP 属地广东