Vue3 的编译模块包含4个目录:
compiler-core // 编译核心Compiler-DOM // 浏览器相关Compiler-sfc // 单文件组件Compiler-SSR // 服务端渲染
其中,compiler-core 模块是Vue编译的核心模块,与平台无关。其余三个基于 compiler-core,适用于不同的平台。
Vue 的编译分为三个阶段,即 解析(Parse)、转换(Transform)和代码生成(Codegen)。
Parse 阶段将模板字符串转换为语法抽象树 AST。Transform 阶段对 AST 做一些转换处理。Codegen 阶段根据 AST 生成相应的渲染函数字符串。
Parse 阶段
分析模板字符串时,Vue 可分为两种情况:以< 开头的字符串,和不是以 < 开头的字符串。
不是以 < 开头的字符串有两种情况:文本节点或者插入表达式 {{exp}}。
使用 < 将字符串的开头分为以下几种情况:
- 元素开始标签
- 元素结束标签
- 注释节点
- 文件声明
用伪代码表示,近似过程如下:
while (s.length) {if (startsWith(s, '{{')) { // 如果开始为 '{{'node = parseInterpolation(context, mode)} else if (s[0] === '<') { // 元素开始标签if (s[1] === '!') {if (startsWith(s, '<!--')) { // 注释节点node = parseComment(context)} else if (startsWith(s, '<!DOCTYPE')) { //文档语句node = parseBogusComment(context)}} else if (s[1] === '/') { // 结束标签parseTag(context, TagType.End, parent)} else if (/[a-z]/i.test(s[1])) { // 开始标签名node = parseElement(context, ancestors)}} else { // 普通文本节点node = parseText(context, mode)}}
原始代码点这里vue-next parse.ts
相对应的几个函数如下:
parseChildren(),入口函数parseInterpolation(),分析双花插值表达式parseComment(),解析注释parseBogusComment(),分析文件声明parseTag(),分析标签parseElement(),分析元素节点,它将在内部执行parseTag()parseText(),分析普通文本parseAttribute(),分析属性
当标签、文本、注释等每个节点生成相应的AST节点时,Vue 将截断解析的字符串。
字符串被截断是使用 AdvanceBy(context,numberOfCharacters)函数,context 是字符串的上下文对象,numberOfCharacters是要截断的字符数。
使用一个简单的示例来模拟截断操作:
<div name="test"><p></p></div>
首先分析 <div,然后执行 advanceBy(context,4) 截断操作(内部执行 s=s.slice(4) ),变成:
name="test"><p></p></div>
然后分析属性,并将其截断为:
<p></p></div>
类似地,以下内容的截断为:
></p></div></div><!-- 所有字符串都已解析 -->
所有 AST 节点定义都在 Compiler-core/astts 文件中,下面是元素节点的定义:
export interface BaseElementNode extends Node {TYPE: NODETYPES.EEMENT / / Type 类型NS: namespace // 名称空间默认为html, ie 0Tag: String // 标签名称tagType: ElementTypes // 元素类型IsselfClosing: boolean // 是否为自闭标记, 例如 <hr />Props: Array <Attribute | DirectiveNode> // 属性, 包含 Html 属性和指令Children: TemplateChildNode [] // 子级模板指向}
用一个比较复杂的例子来解释解析过程。
<div name="test"><!-- This is a comment--><p>{{ test }}</p>A text node<div>good job!</div></div>
上面的模板字符串假定为 S,第一个字符 S[0] 在开始时为 <,这意味着它只能是刚才提到的四种情况之一。
再看看 S[1] 第二个字符的规则:
- 遇到 ! 时,调用字符串原始方法 startsWith(),分析是 <!—的开头, 还是 <!DOCTYPE 的开头,它们对应的处理函数不同,例子中代码最终将解析到注释节点。
- 如果是 / ,按结束标签。
- 如果不是 /,按开始标签处理。
在我们的示例中,这是一个 开始标签。
这里要提到的一点,Vue 将使用栈来保存已解析的元素标签。当遇到开始标记时,标签被推入栈中。当遇到结束标记时,将弹出栈。它的作用是保存已解析但尚未解析完的元素标签。在这个栈中还有另一个角色,通过 stack[stack.length-1] ,可以得到它的父元素。
从我们的例子来看,在解析过程中,栈中存储如下:
1. [div] // div 入栈2. [div, P] // p 入栈3. [div] // P 弹出4. [div, div] // div 入栈5. [div] // div 弹出6. [] // 最后一个div弹出后,模板字符串已解析,栈为空。
按照上面的例子,接下来将截断 <div 字符串,并解析其属性。
属性有两种情况:
- HTML的普通属性
- Vue的指令
生成的类型节点值,HTML 普通属性节点类型为6,Vue 指令节点类型为7。
所有节点类型值详情如下:
Root, // 根节点为 0Element, // 元素节点为 1Text, // 文本节点为 2Comment, // 注释节点为 3Simple_expression, // 简单表达式为 4Interpolation, // 双花插值 {{}} 为 5Attribute, // 属性为 6Directive, // 指令为 7
属性分析后,div 开始标签被分析完毕, 此行字符串被截断。其余字符串现在如下所示:
<!-- This is a comment --><p>{{ test }}</p>A text node<div>good job!</div></div>
注释文本和普通文本节点解析规则比较简单简单,直接截断,生成节点。注释节点调用 parseComment() 函数处理,Text 节点调用 parseText() 处理。
双花插值 {{test}} 的字符串处理逻辑稍微复杂一些:
- 首先提取出双括号内的内容,即 test,调用 trim 函数去掉两边空格。
- 然后生成两个节点,一个节点为 INTERPOLATION 类型值为5,表示它是一个双花插值。
- 第二个节点是其内容 test,将生成节点为 Simple_expression,类型值为4。
return {TYPE: NODETYPES.ITERPOLATION, // 双花括号类型content: {type: NodeTypes.SIMPLE_EXPRESSION, // 简单表达式类型Isstatic: false, // 不是静态节点isConstant: false,content,loc: getSelection(context, innerStart, innerEnd)},loc: getSelection(context, start)}
字符串解析逻辑的其余部分与上述内容类似,因此未对其进行解释。示例解析AST如下所示:

从 AST 中,还可以看到一些节点上的其他属性:
- NS,命名空间,通常为 HTML,值为0
- LOC,它是一条位置消息,指示此节点位于源 HTML字符串的位置,包含行、列、偏移量等信息。
- {{ test }} 解析后的节点将具有 isStatic属性,该值为 false,表示这是一个动态节点。如果是静态节点,则只生成一次,并且会复用相同的节点,不需要进行差异比较。
还有一个标签类型值,它有4个值:
export const enum ElementTypes {ELEMENT, // 0 元素节点Component, // 1 注释节点Slot, // 2 插槽节点Template // 3 模板}
主要用于区分以上四种类型的节点。
Transform 阶段
在转换阶段,Vue 将对 AST 执行一些转换操作,主要是根据 CodeGen阶段 使用的不同 AST节点添加不同的选项参数。以下是一些重要的选项:
cacheHandlers 缓存处理程序
如果 CacheHandlers 的值为 true,则启用函数缓存。例如 @click=”foo” 默认情况下编译为 {onClick:foo},如果打开此选项,则编译为:
{ onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) } // 具备缓存功能
hoistStatic 静态提升
hoistStatic 是一个标识符,表示是否应启用静态节点提升。如果值为 true ,静态节点将被提升在 render() 函数外部,生成名为 _hoisted_x 的变量。
例如,文本 A text node 生成的代码为 const _hoisted_2 = / # __ pure * / createtextVNode (“a text node”)**。
在下面两张图片中,前一张为 hoistStatic=false,后一张为 hoistStatic=true,都可以自己尝试一下 地址。


prefixIdentifiers 前缀标识
此参数的角色用于代码生成。例如,{{ foo }} 模块(module)模式下生成的代码是 _ctx.foo,函数(function)模式下生成的代码是 width(this){…}。因为在模块(module)模式下,默认为严格模式,不能使用 with 语句。
PatchFlags 补丁标识
转换为 AST 节点时,使用 PatchFlag 参数,该参数主要用于差异比较 diff 过程。当 DOM 节点具有此标志且大于0时,它将被更新,并且不会跳过。
来看看 PatchFlag 的值:
export const enum PatchFlags {// 动态文本节点TEXT = 1,// 动态类CLASS = 1 << 1, // 2// 动态StyleSTYLE = 1 << 2, // 4// 动态属性,但不包括 calss 和 style// 如果是组件,则可以包含 calss 和 style。PROPS = 1 << 3, // 8// 具有动态键属性,当键更改时,需要进行完整的 DIFF 差异比较FULL_PROPS = 1 << 4, // 16// 具有侦听事件的节点HYDRATE_EVENTS = 1 << 5, // 32// 不改变子序列的片段STABLE_FRAGMENT = 1 << 6, // 64// 具有key属性的片段或部分子字节具有keyKEYED_FRAGMENT = 1 << 7, // 128// 子节点没有密钥的 keyUNKEYED_FRAGMENT = 1 << 8, // 256// 节点将仅执行 non-PROPS 比较NEED_PATCH = 1 << 9, // 512// 动态插槽DYNAMIC_SLOTS = 1 << 10, // 1024// 静态节点HOISTED = -1,// 退出 DIFF 差异比较优化模式BAIL = -2}
从上面的代码可以看出,PatchFlag 使用 bit-map 来表示不同的值,每个值都有不同的含义。 Vue 会在 diff 过程中根据不同的修补标志使用不同的修补方法。
下图为变换后的 AST:

可以看到 CodegenNode、Helpers 和 Hoists 已填充了相应的值。CodegenNode 是生成要使用的代码的数据。Hoists 存储静态节点。Helpers 存储创建 vNode 的函数名(实际上是 Symbol)。
在正式开始转换之前,需要创建一个 transformContext,即转换上下文。与这三个属性相关的数据和方法如下:
helpers: new Set(),hoists: [],// methodshelper(name) {context.helpers.add(name)return name},helperString(name) {return `_${helperNameMap[context.helper(name)]}`},hoist(exp) {context.hoists.push(exp)const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`,false,exp.loc,true)identifier.hoisted = expreturn identifier},
让我们来看看具体的转换过程是如何使用的。用 举例说明。
{{ test }}
此节点对应 TransformElement() 转换函数,因为 p 没有绑定动态属性,没有绑定指令,所以焦点不在它上面。而 {{test}} 是一个双花插值表达式,所以将其 patchflag 设置为1(动态文本节点),相应的执行代码 patchFlag |=1。然后执行 createVNodeCall() 函数,其返回值为该节点的 codegennode 值。
node.codegenNode = createVNodeCall(context,vnodeTag,vnodeProps,vnodeChildren,vnodePatchFlag,vnodeDynamicProps,vnodeDirectives,!!shouldUseBlock,false /* disableTracking */,node.loc)
createVNodeCall() 会相应的在 createVNode() 中添加一个符号,它放置在 helpers 中。事实上,helpers 功能将在代码生成阶段引入。
// createVNodeCall () 内部执行过程,多余代码已删除context.helper(CREATE_VNODE)return {type: NodeTypes.VNODE_CALL,tag,props,children,patchFlag,dynamicProps,directives,isBlock,disableTracking,loc}
hoists 提升
是否将节点提升,主要看它是否是静态节点。
<div name = "test"> // 静态属性节点<! - This is a comment-><p>{{ test }}</p>A text node // 静态节点<div> good job! </div> // 静态节点</div>
可以看到,上面有三个静态节点,因此 hoists 数组有3个值。注释为什么不算静态节点,暂时还没有找到原因。。。
TYPE changes 类型改变

从上图中可以看出,最外层 div 的类型为1,由 Transform 生成的 CodeGen node 中的类型为13。
这13是 VNODE_CALL 对应的类型值,其他还有:
// codegenVNODE_CALL, // 13JS_CALL_EXPRESSION, // 14JS_OBJECT_EXPRESSION, // 15JS_PROPERTY, // 16JS_ARRAY_EXPRESSION, // 17JS_FUNCTION_EXPRESSION, // 18JS_CONDITIONAL_EXPRESSION, // 19JS_CACHE_EXPRESSION, // 20
刚才提到的例子 {{ test }}, 其 codegen node是 createVnodeCall 函数生成。
return {type: NodeTypes.VNODE_CALL,tag,props,children,patchFlag,dynamicProps,directives,isBlock,disableTracking,loc}
从上面的代码可以看出,type 设置为 nodetypes.VNODE_CALL,即13。
每个不同的节点由不同的变换函数处理。可以自己再深入的了解。
Codegen阶段
代码生成阶段最后生成了一个字符串,去掉了字符串的双引号,具体内容是什么:
const _Vue = Vueconst { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vueconst _hoisted_1 = { name: "test" }const _hoisted_2 = / * # __ pure __ * / _ createtextVNode ("a text node")const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)return function render(_ctx, _cache) {with (_ctx) {const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vuereturn (_openBlock(), _createBlock("div", _hoisted_1, [_CreateCommentVNode ("This is a comment"),_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */),_hoisted_2,_hoisted_3]))}}
代码生成模式
可以看到上面的代码最终返回了 render() 函数,生成相应的 VNODE。
实际上,代码生成有两种模式:模块和函数。选取哪种模式由前缀标识符决定。
函数模式功能:使用 const {helpers…}=Vue 获取帮助函数的方法,即 createVode()、createCommentVNode() 这些函数,最后返回 render() 函数。
模块模式为:使用ES6模块导入导出功能,即 import 和 export。
Static node 静态节点
此外,还有三个变量。以 hoisted 命名,后面跟数字,表示这是静态变量。
看看解析阶段的 HTML 模板字符串:
<div name="test"><! - This is a comment-><p>{{ test }}</p>A text node<div>good job!</div></div>
这个示例只有一个动态节点,即 {{test},其余的都是静态节点。从生成的代码中还可以看出,生成的节点和模板中的代码对应于一个或多个节点。静态节点的作用是只生成一次,以后直接重用。
细心的你可能会发现 Highed_2 和 Highed_3 变量有一个 /#PURE/ 的注释。
此注释的作用是表明此功能是纯功能,无副作用,主要用于Tree-shaking 。压缩工具将直接从打包时未使用的代码中删除。
来看下一代动态节点,{{ test }} 生成代码对应为 createVNode(“p”, null, _toDisplayString(test), 1 / TEXT _/)。
其中,_toDisplayString(test) 的内部实现是:
return val == null? '': isObject(val)? JSON.stringify(val, replacer, 2): String(val)
该代码非常简单,它是一个字符串转换输出。
createVNode(“p”, null, _toDisplayString(test), 1 / TEXT _/) 的最后一个参数增加转换时的 Patchflag 值。
Help function 辅助函数
在 Transform 和 Codegen 阶段,都看到了 helpers 辅助函数的影子,它是什么呢?
Name mapping for runtime helpers that need to be imported from 'vue' ingenerated code. Make sure these are correctly exported in the runtime!Using `any` here because TS doesn't allow symbols as index type.// 需要从生成代码中的“vue”导入的运行时帮助程序的名称映射。// 确保这些文件在运行时正确导出!// 此处使用'any',因为TS不允许将符号作为索引类型。export const helperNameMap: any = {[FRAGMENT]: `Fragment`,[TELEPORT]: `Teleport`,[SUSPENSE]: `Suspense`,[KEEP_ALIVE]: `KeepAlive`,[BASE_TRANSITION]: `BaseTransition`,[OPEN_BLOCK]: `openBlock`,[CREATE_BLOCK]: `createBlock`,[CREATE_VNODE]: `createVNode`,[CREATE_COMMENT]: `createCommentVNode`,[CREATE_TEXT]: `createTextVNode`,[CREATE_STATIC]: `createStaticVNode`,[RESOLVE_COMPONENT]: `resolveComponent`,[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,[RESOLVE_DIRECTIVE]: `resolveDirective`,[WITH_DIRECTIVES]: `withDirectives`,[RENDER_LIST]: `renderList`,[RENDER_SLOT]: `renderSlot`,[CREATE_SLOTS]: `createSlots`,[TO_DISPLAY_STRING]: `toDisplayString`,[MERGE_PROPS]: `mergeProps`,[TO_HANDLERS]: `toHandlers`,[CAMELIZE]: `camelize`,[CAPITALIZE]: `capitalize`,[SET_BLOCK_TRACKING]: `setBlockTracking`,[PUSH_SCOPE_ID]: `pushScopeId`,[POP_SCOPE_ID]: `popScopeId`,[WITH_SCOPE_ID]: `withScopeId`,[WITH_CTX]: `withCtx`}export function registerRuntimeHelpers(helpers: any) {Object.getOwnPropertySymbols(helpers).forEach(s => {helperNameMap[s] = helpers[s]})}
事实上,帮助函数是 Vue 在代码生成时引入的一些函数,因此程序可以正常执行,从上面生成的代码可以看出。helperNameMap 是默认的映射表名,它是要从 Vue 引入的函数名。
此外,我们还可以看到一个注册函数。registerRuntimeHelpers(helpers: any() 是做什么用的呢?
我们知道编译模块的编译器核心是一个独立于平台的,而编译Dom是一个与浏览器相关的编译模块。要在浏览器中运行 Vue 程序,请导入与浏览器相关的 Vue 数据和功能。
registerRuntimeHelpers(helpers: any() 用于执行此操作,可以从 Compiler-dom 的 runtimehelpers.ts 文件中看到:
registerRuntimeHelpers({[V_MODEL_RADIO]: `vModelRadio`,[V_MODEL_CHECKBOX]: `vModelCheckbox`,[V_MODEL_TEXT]: `vModelText`,[V_MODEL_SELECT]: `vModelSelect`,[V_MODEL_DYNAMIC]: `vModelDynamic`,[V_ON_WITH_MODIFIERS]: `withModifiers`,[V_ON_WITH_KEYS]: `withKeys`,[V_SHOW]: `vShow`,[TRANSITION]: `Transition`,[TRANSITION_GROUP]: `TransitionGroup`})
运行 registerRuntimeHelpers(helpers: any() 映射表被注入与浏览器相关的函数。
如何使用这些辅助函数?
在解析阶段,解析不同节点时会生成相应的类型。
在转换阶段,生成一个辅助对象,它是一个集合数据结构。每当转换 AST 时,都会根据 AST 节点的类型添加不同的帮助器函数。
例如,假设现在正在转换注释节点,它将执行 context.helper(CREATE_COMMENT) ,内部通过 helpers.add(‘createCommentVNode’) 添加。
然后在 Codegen 阶段,遍历 helpers,从 Vue 导入所需的函数,代码实现如下:
// 这是模块模式`import { ${ast.helpers.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`).join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
如何生成代码?
从 Codegen.ts 文件中,可以看到许多代码生成函数:
Generate () // 入口文件GenfunctionExpression () // 生成函数表达式Gennode () // 生成vNode节点...
生成代码是基于不同的 AST 节点调用不同的代码生成函数,最后将代码字符串拼合在一起,输出完整的代码字符串。
老规矩,还是看一个例子:
const _hoisted_1 = { name: "test" }const _hoisted_2 = / * # __ pure __ * / _ createtextVNode ("a text node")const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
看看这段代码是如何生成的,内部会执行 genHoists(ast.hoists, context), 提升的静态节点作为第一个参数,genHoists() 内部简化实现:
hoists.forEach((exp, i) => {if (exp) {push(`const _hoisted_${i + 1} = `);genNode(exp, context);newline();}})
从上面的代码可以看出,遍历 hoists 数组,调用 genNode(exp, context) 函数。genNode() 根据不同的类型执行不同的功能。
const _hoisted_1 = { name: "test" }
这一行的 const _hoisted_1 = 通过 genHoists() 函数生成,{ name: “test” } 是通过 genObjectExpression() 函数生成。
