Vue源码解析系列-模板编译

模板编译

浏览器其实并不能理解vue的模板语法,vue实际上是先利用编译器(compiler)在编译阶段实现模板编译,即将模板语法转换成vnode,然后在通过patch操作修改DOM来达到更改视图的目的。
模板编译包含几个重要操作和中间产物,大致流程如下:
template -> (parse) -> ast -> (transform) => codegenNode -> (generate) -> render函数
三个重要的步骤:
  • parse: 解析template生成token并构建模板AST。
  • transform: 将模板AST转换成能够生成JavaScript渲染函数代码的AST(codegenNode)
  • generate:根据前面生成的AST来生成渲染函数代码。

解析模板并构建AST的思路

HTML解析规范

事实上,解析html并构造token是有规范的,见HTML规范
vue使用有限状态自动机来解析模板。所谓“有限状态”,就是指有限个状态,而“自动机”意味着随着字符的输入,解析器会自动地在不同状态间迁移。
notion image
例如上面的Data 状态,它规定了在当前状态下,不同的下个输入字符所能进入的不同状态以及相应的处理方法。像这样的状态有很多,解析过程就在这些状态中不停的流转,直到到达结尾。在这个过程中会创建很多token,利用这些token可以构建ast。
另外除了state外,HTML规范还定义了模式(mode),规则如下:
  • Data state。解析的初始模式。
  • <title> 标签、<textarea> 标签,当解析器遇到这两个标签时,会切换到 RCDATA 模式;
  • <style><xmp><iframe><noembed><noframes><noscript> 等标签,当解析器遇到这些标签时,会切换到 RAWTEXT 模式;
  • 当解析器遇到 <![CDATA[ 字符串时,会进入 CDATA 模式。
其他模式与Data模式的解析方式相似,简单起见,这里只考虑Data state。
以一个简单的html来说明解析流程:
<h1>Vue</h1>
解析过程是这样:
  1. 初始是Data state,读取下一个输入字符,发现是<,进入tag open state
  1. 读取下一个输入字符,是ASCII码h,创建一个新的start tag token,将其标签名称设置为空字符串,切换到tag name state,并将当前字符作为下一个输入字符。
  1. 读取下一个输入字符,是ASCII码h,将当前输入字符的小写版本添加到当前tag token的标签名称,不流转状态。
  1. 读取下一个输入字符,是ASCII码1,将当前输入字符的小写版本添加到当前tag token的标签名称,不流转状态。
  1. 读取下一个输入字符,发现是> ,发送当前tag token,进入data state
  1. 读取下一个输入字符,发现是V,将该字符作为character token 发送 。
  1. 读取下一个输入字符,发现是u,将该字符作为character token 发送 。
  1. 读取下一个输入字符,发现是e,将该字符作为character token 发送 。
  1. 读取下一个输入字符,发现是<,进入tag open state
  1. 读取下一个输入字符,发现是/ ,进入end tag open state
  1. 读取下一个输入字符,发现是ASCII码h,创建一个新的end tag token,将其标签名称设置为空字符串,切换到tag name state,并将当前字符作为下一个输入字符。
  1. 读取下一个输入字符,发现是ASCII码h,将当前输入字符的小写版本添加到当前tag token的标签名称,不流转状态。
  1. 读取下一个输入字符,发现是ASCII码1,将当前输入字符的小写版本添加到当前tag token的标签名称,不流转状态。
  1. 读取下一个输入字符,发现是> ,发送当前tag token,进入data state
  1. 读取下一个输入字符串,EOF,发现读取完毕,发送end-of-file token
在上面的例子中我们可以看到每次都只取出一个字符进行判断,并且每次判断完后都会自动读取下一个字符再判断(某些情况会将当前字符重新作为下一个字符),假设当前组件模板字符串存于context.source中,我们可以通过不断的删除前面已经判断过的字符来使每次都能通过context.source[0]获取到下一个字符。
while(context.source){ if(mode=='data' && context.source[0] === '<'){ mode = 'tag open' // ... }else if(mode == 'tag open' && /[a-z]/.test(context.source[0])){ mode = 'tag name' // ... }else if(context.source[0] === '>'){ mode = 'data' // ... } // ... context.source = context.source.slice(1) }

判断是否是自闭和标签

HTML中有些标签是自闭和的,例如<br /> 。假如该标签是自闭和标签,则表示它没有闭合标签,解析方式与闭和标签不同,因此我们在解析标签时还得判断其是否是自闭和标签。
判断的方式有两种,一种是设立自闭和标签名单,然后判断标签名是否在自闭和名单之中。
另一种更简单的方法是判断标签是否以/>结束。
let isSelfClosing = false isSelfClosing = startsWith(context.source, '/>')

解析插值语法和attribute绑定

通过HTML规范我们已经能够实现解析HTML,但是vue的template与HTML并不完全相同,vue template对HTML做了增强,例如插值语法({{}})、Attribute 绑定等。
判断是否是插值语法很简单,只需要在文本状态时读取是否有{{即可,当读到}}表示插值语法结束。
而attribute属性也有规律可循,首先attribute属性是在标签内部,其次是name=value的形式,例如<a href=”http://123.com” > ,因此我们可以用正则来获取name和value,判断是否是attribute绑定也很简单,直接判断是否以:v-on等开始即可。
// Name. const start = getCursor(context) const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)! // 假设此时context.source = ':href="">',可以匹配到:href const name = match[0] // 删除已匹配的name和= context.source = context.source.slice(name.length + 1) if (!context.inVPre && /^(v-|:|\.|@|#)/.test(name)) { // 如果attribute有使用绑定,则需要进一步处理 }
value的处理分三种情况:被单引号包裹、被双引号包裹、没有引号包裹,例如a='a'b="b"c=c 。被引号包裹的两种情况容易处理,第三种情况复杂一些,因为属性之间由空格、制表符等符号分隔,所以可以通过正则表达式/^[^\t\r\n\f >]+/匹配到从当前字符到下个属性或标签结束标志(>号)前的内容,例如foo=value a=c> ,因为name和=被使用过删除了,因此剩下value a=c>,正则表达式/^[^\t\r\n\f >]+/能匹配到value。
这里还要注意,如果是不被引号包裹,那么value就不能再出现引号等特殊符号。例如foo=f"oot",这可能是用户书写错误,可以用正则表达式/["'<=`]/g提取并发送错误。
// a='a' b="b" c=c const quote = context.source[0] const isQuoted = quote === `"` || quote === `'` if (isQuoted) { // value被引号包裹的情况 const endIndex = context.source.indexOf(quote) context.source = context.source.slice(1) // 删除左侧的引号 if (endIndex === -1) { // 如果没有右侧的引号,则将source后面的所有内容都作为attribute content = parseTextData( context, context.source.length, TextModes.ATTRIBUTE_VALUE ) } else { // 如果有右侧的引号,则提取引号内的value content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE) } } else{ // 没有引号包裹的情况 const match = /^[^\t\r\n\f >]+/.exec(context.source); if (!match) { return undefined; } const unexpectedChars = /["'<=`]/g; let m; while ((m = unexpectedChars.exec(match[0]))) { // 将引号的位置发送给错误处理机制,提醒用户。 emitError( context, ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE, m.index ) } content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE) }
属性解析阶段结束的标志应当是下一个输入字符为>或者/>的时候。所以整个属性解析过程应该是在while循环的内部。
while ( context.source.length > 0 && !startsWith(context.source, '>') && !startsWith(context.source, '/>') ) { const attr = parseAttribute(context, attributeNames) // ... }

构建AST

通过上一节的介绍,我们已经能够解析template,下一步就是根据解析过程中生成的token来构建AST。
以下面这个例子:
<h1><span>Vue</span></h1>
通过解析获得了下面的tokens:
[ {type: "start tag", tagName: "h1"}, {type: "start tag", tagName: "span"}, {type: "text", value: "Vue"}, {type: "end tag", tagName: "span"}, {type: "end tag", tagName: "h1"}, {type: "end-of-file"} ]
ast是树结构,我们需要扫描tokens来构建这颗树,这里需要借助栈结构。每次从tokens出队一个节点,如果type是开始标签,则作为栈顶的子节点,并将其添加进栈结构中,作为下一个节点的父节点,如果type是结束标签,则移除栈顶节点,当type为EOF时结束。
const tokens = [ { type: "start tag", tagName: "h1" }, { type: "start tag", tagName: "span" }, { type: "text", value: "Vue" }, { type: "end tag", tagName: "span" }, { type: "end tag", tagName: "h1" }, {type: "end-of-file"} ]; const root = {type: "root",children: []} const nodeStack = [root]; while (nodeStack.length) { const parent = nodeStack[nodeStack.length-1]; const currentNode = tokens.shift(); if (currentNode.type === 'start tag') { const element = {type: 'element', tag: currentNode.tagName, children:[]} parent.children.push(element); nodeStack.push(element) } else if (currentNode.type === 'text') { const textNode = {type: 'text', value: currentNode.value} parent.children.push(textNode) } else if (currentNode.type === 'end tag') { nodeStack.pop() } else if(currentNode.type === 'end-of-file'){ break; } } console.log(root);

vue-compiler源码

在实际的vue源码中,由compiler来完成从解析模板到生成渲染函数的整个过程。
在vue的源码中有4个负责compiler的package,分别是:
  • compiler-core
  • compiler-dom
  • compiler-sfc
  • compiler-ssr
compiler-core存放compile核心逻辑代码,这部分代码无关运行环境,compiler-sfc 提供转换sfc为渲染函数的底层API,而compiler-ssrcompiler-dom则是分别提供SSR环境下和DOM环境下的编译API。
compiler-ssr这部分不展开讲
vue编译器最主要的作用就是vue模板转换成渲染函数,然后再由vue渲染器渲染成DOM。
vue编译器的主要步骤是:
parse函数将vue单页面组件转换成模板ast,然后再调用transform函数将模板ast进行转换,最后调用generator函数将ast转换成渲染函数。

parse

vue解析模板最核心的代码存放在@vue/compiler-core下的parse.ts文件中。
vue解析模板的入口是baseParse函数。
export function baseParse( content: string, options: ParserOptions = {} ): RootNode { const context = createParserContext(content, options) const start = getCursor(context) return createRoot( parseChildren(context, TextModes.DATA, []), getSelection(context, start) ) }
其中最重要的解析逻辑位于parseChildren函数中,其实parseChildren函数就相当于一个状态机了。
实际上vue源码要比我们前面写的代码复杂一些,因为vue考虑的情况更多,node的种类也比较多。下面是解析element和text生成的token。
// 解析标签 function parseTag( context: ParserContext, type: TagType, parent: ElementNode | undefined ): ElementNode | undefined{ // ... return { type: NodeTypes.ELEMENT, // 节点类型,例如root、element、text、comment等 ns, // Namespace tag, // 标签名 tagType, // element、component、slot、template props, isSelfClosing, // 是否是自闭和标签 children: [], loc: getSelection(context, start), codegenNode: undefined // to be created during transform phase } } // 解析文本 function parseText(context: ParserContext, mode: TextModes): TextNode { // ... return { type: NodeTypes.TEXT, content, loc: getSelection(context, start) } }
vue定义了很多函数来表示html解析状态,例如:
  • parseChildren: 代表data state
  • parseElement:包含tag open statetag end state 两个阶段。
  • parseTag: 代表tag name state
  • parseText: 解析文本。
按照HTML规范,每当进入另一个状态时,在代码的体现就是调用对应的函数。例如从data statetag open state,实际上就是在parseChildren中调用parseElement函数。
// 注意,为了方便理解,省略了部分不重要的代码 function parseChildren( context: ParserContext, mode: TextModes, ancestors: ElementNode[] ): TemplateChildNode[] { // ancestors是一个栈,调用last函数获取这个栈的栈顶元素 const parent = last(ancestors) const ns = parent ? parent.ns : Namespaces.HTML const nodes: TemplateChildNode[] = [] while (!isEnd(context, mode, ancestors)) { const s = context.source let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined if (mode === TextModes.DATA || mode === TextModes.RCDATA) { if (!context.inVPre && startsWith(s, context.options.delimiters[0])) { // 插值语法, 即匹配'{{' node = parseInterpolation(context, mode) } else if (mode === TextModes.DATA && s[0] === '<') { // Tag open state if (s.length === 1) { emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1) } else if (s[1] === '!') { // markup declaration open state. if (startsWith(s, '<!--')) { // 注释节点 node = parseComment(context) } else if (startsWith(s, '<!DOCTYPE')) { // 忽略文档声明. node = parseBogusComment(context) } else { emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT) node = parseBogusComment(context) } } else if (/[a-z]/i.test(s[1])) { // tag open state node = parseElement(context, ancestors) } else { emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1) } } } if (!node) { // 解析文本 node = parseText(context, mode) } if (isArray(node)) { for (let i = 0; i < node.length; i++) { pushNode(nodes, node[i]) } } else { pushNode(nodes, node) } } // ... return nodes }
当出现不合规范的模板语法时,vue会通过emitError来发送error 提示用户,context对象包含当前解析字符的信息,包含source(模板字符串)、line(当前字符第几行)、column(当前字符第几列)、offset(当前字符串的偏移量)等,vue的错误处理机制会通过这些信息将错误代码复现给用户,以便快速找到错误。
ancestors事实上就相当于我们前面所说的nodeStack栈,通过这个栈来构建ast树结构,last(ancestors)实际上就是获取这个栈的栈顶元素,这个栈顶元素就是当前节点的父节点。
vue不会先扫描完source生成所有的token后再构建ast,而是边扫描source生成token边构建ast。

transform

通过parse操作我们已经成功地将模板语法转换成了AST(抽象语法树),我们的最终目的是要将其转换成渲染函数代码,因此需要将前面的模板AST进行转换,以便后面更好地生成渲染函数代码。我们设计一个transform函数来实现。
transform函数位于@vue/compiler-core包下transform.ts文件中。
前面我们讲过,ast是一个N叉树,那么我们要将其转换就应该对它进行深度优先遍历。我们使用traverseNode函数实现深度遍历逻辑,为了方便,我们将循环遍历node.children的操作放到traverseChildren函数里。
function traverseNode(node, context) { context.currentNode = node; context.childrenIndex = 0; /* 处理逻辑 */ if (node.type === 'element' && node.props.find(i=>i.name==='if')) { // ... } if (node.type === 'element' && node.props.find(i=>i.name==='slot')) { // ... } // 这里省略一大段代码 // if ... // 深度优先遍历 if (node.type === "element" || node.type === "root") { traverseChildren(node.context); } else if (node.type === "comment") { // ... } context.currentNode = node } function traverseChildren(parent, context) { for (let i = 0; i < parent.children.length; i++) { const child = parent.children[i]; context.parent = parent; context.childIndex = i; traverseNode(child, context); } } const nodes = [ { type: "root", children: [ { type: "element", tag: "div", children: [ { type: "element", tag: 'h1', props: [{ name: "foo", value: "value" }], children: [ { type: "text", value: "Hello World", }, ], }, ], }, { type: "element", tag: "br", children: [], }, ], }, ]; traverseNode(nodes, context)
随着转换逻辑越来越多,越来越复杂,我们的traverseNode 函数也会越来越臃肿,因此我们可以将这些转换逻辑提取到一个个函数中去。更好的办法是以插件化的形式来实现,也就是将这些转换函数当成一个个插件放到一个插件数组中去,然后循环调用这个插件数组中的每一个插件函数,这个插件数组就是context.nodeTransforms
在执行这些插件函数时会将上下文context对象一同传入,我们可以提前将一些信息挂载到这个上下文对象中,例如将当前处理的node对象挂载到context.currentNode中,之后在插件函数执行时就可以通过这个context对象来获取一些我们想要的信息,例如当前节点类型context.currentNode.type
function traverseNode(node, context) { context.currentNode = node; context.childrenIndex = 0; /* 处理逻辑 */ const { nodeTransforms } = context; for (let i = 0; i < nodeTransforms.length; i++) { nodeTransforms[i](node); } // ... } traverseNode(nodes, { nodeTransforms: [ transformOnce, transformIf, transformMemo, transformFor, ...(__COMPAT__ ? [transformFilter] : []), ...(!__BROWSER__ && prefixIdentifiers ? [ // order is important trackVForSlotScopes, transformExpression, ] : __BROWSER__ && __DEV__ ? [transformExpression] : []), transformSlotOutlet, transformElement, trackSlotScopes, transformText, ], });

generate

现在就只差最后一步了,那就是生成渲染函数代码,这部分由generate函数完成。
generate的逻辑存放在@vue/compiler-core包下的codegen.ts文件中。

codegenNode

transform后的ast存放在ast.codegenNode属性中。例如下面这段代码,经过parse和transform后会变成这样:
<div> <h1 :foo="num">Hello World</h1> </div>
codegenNode = { type: 13, tag: "div", props: undefined, children: [ { type: 1, ns: 0, tag: "h1", tagType: 0, props: [ { type: 7, name: "bind", exp: { type: 4, content: "_ctx.num", isStatic: false, constType: 0, loc: { // ... source: "num", }, }, arg: { type: 4, content: "foo", isStatic: true, constType: 3, loc: { // ... source: "foo", }, }, modifiers: [ ], loc: { // ... source: ":foo=\"num\"", }, }, ], isSelfClosing: false, children: [ { type: 2, content: "Hello World", loc: { // ... source: "Hello World", }, }, ], loc: { // ... source: "<h1 :foo=\"num\">Hello World</h1>", }, codegenNode: { type: 13, tag: "\"h1\"", props: { type: 15, loc: { // ... source: "<h1 :foo=\"num\">Hello World</h1>", }, properties: [ { type: 16, loc: { source: "", // ... }, key: { type: 4, content: "foo", isStatic: true, constType: 3, loc: { // ... source: "foo", }, }, value: { type: 4, content: "_ctx.num", isStatic: false, constType: 0, loc: { // ... source: "num", }, }, }, ], }, children: { type: 2, content: "Hello World", loc: { // ... source: "Hello World", }, }, patchFlag: "8 /* PROPS */", dynamicProps: { type: 4, loc: { source: "", // ... }, content: "_hoisted_1", isStatic: false, constType: 2, hoisted: { type: 4, loc: { source: "", // ... }, content: "[\"foo\"]", isStatic: false, constType: 0, }, }, directives: undefined, isBlock: false, disableTracking: false, isComponent: false, loc: { // ... source: "<h1 :foo=\"num\">Hello World</h1>", }, }, }, ], patchFlag: undefined, dynamicProps: undefined, directives: undefined, isBlock: true, disableTracking: false, isComponent: false, loc: { // ... source: "<div>\r\n\t\t<h1 :foo=\"num\">Hello World</h1>\r\n\t</div>", }, }
codegenNode存放着能够生成渲染函数的一切信息,我们可以通过遍历codegenNode来生成渲染函数代码。
在经过generate后会生成最终的渲染函数代码:
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("h1", { foo: _ctx.num }, "Hello World", 8 /* PROPS */, ["foo"]) ])) }
可以在vue模板编译预览网站查看Vue Template Explorer (vuejs.org)模板编译结果,在控制台可以看到AST。
首先渲染函数以函数声明开头,因此我们可以直接写出:
const functionName = ssr ? `ssrRender` : `render`; const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']; if (options.bindingMetadata && !options.inline) { // binding optimization args args.push('$props', '$setup', '$data', '$options'); } const signature = options.isTS ? args.map(arg => `${arg}: any`).join(',') : args.join(', '); push(`function ${functionName}(${signature}) {`); indent() push(`return `) genNode(ast.codegenNode, context) deindent() push(`}`) function push(code){ context.code += code; // 将代码添加到context.code中 // 。。。省略一些处理 }
functionName是渲染函数的函数名,signature 则是函数形参。push函数主要的作用是将code代码添加到context.code中,indentdeindent函数则是为生成的context.code增加/删除缩进。

genNode()

genNode函数则是生成渲染函数内部逻辑的方法。
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) { if (isString(node)) { context.push(node) return } if (isSymbol(node)) { context.push(context.helper(node)) return } switch (node.type) { case NodeTypes.ELEMENT: case NodeTypes.IF: case NodeTypes.FOR: __DEV__ && assert( node.codegenNode != null, `Codegen node is missing for element/if/for node. ` + `Apply appropriate transforms first.` ) genNode(node.codegenNode!, context) break case NodeTypes.TEXT: genText(node, context) break case NodeTypes.SIMPLE_EXPRESSION: genExpression(node, context) break case NodeTypes.INTERPOLATION: genInterpolation(node, context) break case NodeTypes.TEXT_CALL: genNode(node.codegenNode, context) break case NodeTypes.COMPOUND_EXPRESSION: genCompoundExpression(node, context) break case NodeTypes.COMMENT: genComment(node, context) break case NodeTypes.VNODE_CALL: genVNodeCall(node, context) break // ... default: if (__DEV__) { assert(false, `unhandled codegen node type: ${(node as any).type}`) // make sure we exhaust all possible types const exhaustiveCheck: never = node return exhaustiveCheck } } }
codegenNode 本质上是一颗N叉树,因此vue采用递归的方式进行深度遍历。例如在创建元素vnode时(即生成_createElementVNode函数时),genNode会调用genVNodeCall函数(此时node.type===NodeTypes.VNODE_CALL),genVNodeCall函数内部会调用genNodeList函数,而genNodeList函数又会遍历node.children并将其作为参数传入genNode函数中。
function genVNodeCall(node: VNodeCall, context: CodegenContext) { const { push, helper, pure } = context const { tag, props, children, patchFlag, dynamicProps, directives, isBlock, disableTracking, isComponent } = node // ... genNodeList( genNullableArgs([tag, props, children, patchFlag, dynamicProps]), context ) push(`)`) // ... } function genNodeList( nodes: (string | symbol | CodegenNode | TemplateChildNode[])[], context: CodegenContext, multilines: boolean = false, comma: boolean = true ) { const { push, newline } = context for (let i = 0; i < nodes.length; i++) { const node = nodes[i] if (isString(node)) { push(node) } else if (isArray(node)) { genNodeListAsArray(node, context) } else { genNode(node, context) } if (i < nodes.length - 1) { if (multilines) { comma && push(',') newline() } else { comma && push(', ') } } } }

helper()

vue渲染函数内部会调用不同的函数来创造不同类型的vnode,而这些函数名都保存在helperNameMap中。
export const helperNameMap: any = { [FRAGMENT]: `Fragment`, [TELEPORT]: `Teleport`, [SUSPENSE]: `Suspense`, [KEEP_ALIVE]: `KeepAlive`, [BASE_TRANSITION]: `BaseTransition`, [OPEN_BLOCK]: `openBlock`, [CREATE_BLOCK]: `createBlock`, [CREATE_ELEMENT_BLOCK]: `createElementBlock`, [CREATE_VNODE]: `createVNode`, [CREATE_ELEMENT_VNODE]: `createElementVNode`, [CREATE_COMMENT]: `createCommentVNode`, [CREATE_TEXT]: `createTextVNode`, [CREATE_STATIC]: `createStaticVNode`, [RESOLVE_COMPONENT]: `resolveComponent`, [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`, [RESOLVE_DIRECTIVE]: `resolveDirective`, [RESOLVE_FILTER]: `resolveFilter`, [WITH_DIRECTIVES]: `withDirectives`, [RENDER_LIST]: `renderList`, [RENDER_SLOT]: `renderSlot`, [CREATE_SLOTS]: `createSlots`, [TO_DISPLAY_STRING]: `toDisplayString`, [MERGE_PROPS]: `mergeProps`, [NORMALIZE_CLASS]: `normalizeClass`, [NORMALIZE_STYLE]: `normalizeStyle`, [NORMALIZE_PROPS]: `normalizeProps`, [GUARD_REACTIVE_PROPS]: `guardReactiveProps`, [TO_HANDLERS]: `toHandlers`, [CAMELIZE]: `camelize`, [CAPITALIZE]: `capitalize`, [TO_HANDLER_KEY]: `toHandlerKey`, [SET_BLOCK_TRACKING]: `setBlockTracking`, [PUSH_SCOPE_ID]: `pushScopeId`, [POP_SCOPE_ID]: `popScopeId`, [WITH_SCOPE_ID]: `withScopeId`, [WITH_CTX]: `withCtx`, [UNREF]: `unref`, [IS_REF]: `isRef`, [WITH_MEMO]: `withMemo`, [IS_MEMO_SAME]: `isMemoSame` }
vue通过调用helper函数来获取这些函数名,例如_createCommentVNode
function helper(key) { return `_${helperNameMap[key]}`; } function genComment(node: CommentNode, context: CodegenContext) { const { push, helper, pure } = context if (pure) { push(PURE_ANNOTATION) } push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node) }

Block

渲染函数和我们平常使用的h函数它们的目的都是为了创建vnode。但是经过模板编译生成的渲染函数比h函数多了一个令人困惑的Block
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("h1", { foo: _ctx.num }, "Hello World", 8 /* PROPS */, ["foo"]) ])) }
关于这个Block的作用,在vue官网由提及: 渲染机制 | Vue.js (vuejs.org)
这里我们引入一个概念“区块”(Block),内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if或者 v-for)。
每一个块都会追踪其所有带更新类型标记的后代节点 (不只是直接子节点)。所谓的更新标记是指patchFlag ,例如上面代码中patchFlag就是8 。patchFlag的作用是为了提升虚拟 DOM 运行时性能,一个元素可以有多个更新类型标记,它们会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作。
假如这里使用v-if等结构性指令,那么模板编译后的渲染函数是这样的:
<div> <h1 v-if="isShow">Hello World</h1> </div> // 编译后的代码 import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ (_ctx.isShow) ? (_openBlock(), _createElementBlock("h1", { key: 0 }, "Hello World")) : _createCommentVNode("v-if", true) ])) }
openBlock必须要在“createBlock”前调用,它会初始化currentBlock,并将该currentBlock加入到blockStack中。由于Block树是能够嵌套的,因此用一个栈结构来存放所有的Block树。
// Since v-if and v-for are the two possible ways node structure can dynamically // change, once we consider v-if branches and each v-for fragment a block, we // can divide a template into nested blocks, and within each block the node // structure would be stable. This allows us to skip most children diffing // and only worry about the dynamic nodes (indicated by patch flags). export const blockStack: (VNode[] | null)[] = [] export let currentBlock: VNode[] | null = null /** * Open a block. * This must be called before `createBlock`. It cannot be part of `createBlock` * because the children of the block are evaluated before `createBlock` itself * is called. The generated code typically looks like this: * * ```js * function render() { * return (openBlock(),createBlock('div', null, [...])) * } * ``` * disableTracking is true when creating a v-for fragment block, since a v-for * fragment always diffs its children. * * @private */ export function openBlock(disableTracking = false) { blockStack.push((currentBlock = disableTracking ? null : [])) } export function closeBlock() { blockStack.pop() currentBlock = blockStack[blockStack.length - 1] || null }
后面的createElementBlock函数内部会先调用createBaseVNode函数创建vnode,然后再调用setupBlock函数。此外渲染函数中用于创建元素vnode的createElementVNode函数其实就是createBaseVNode 函数的别名。
// createElementVNode函数其实就是createBaseVNode函数的别名 export { createBaseVNode as createElementVNode } export function createElementBlock( type: string, props?: Record<string, any> | null, children?: any, patchFlag?: number, dynamicProps?: string[], shapeFlag?: number ) { return setupBlock( createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */ ) ) } function createBaseVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag = 0, dynamicProps: string[] | null = null, shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT, isBlockNode = false, needFullChildrenNormalization = false ) { // ... // track vnode for block tree if ( isBlockTreeEnabled > 0 && // avoid a block node from tracking itself !isBlockNode && // has current parent block currentBlock && // presence of a patch flag indicates this node needs patching on updates. // component nodes also should always be patched, because even if the // component doesn't need to update, it needs to persist the instance on to // the next vnode so that it can be properly unmounted later. (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && // the EVENTS flag is only for hydration and if it is the only flag, the // vnode should not be considered dynamic due to handler caching. vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS ) { currentBlock.push(vnode) } return vnode } function setupBlock(vnode: VNode) { // save current block children on the block vnode vnode.dynamicChildren = isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null // close block closeBlock() // a block is always going to be patched, so track it as a child of its // parent block if (isBlockTreeEnabled > 0 && currentBlock) { currentBlock.push(vnode) } return vnode }
可以看到currentBlock在追踪当前Block树的所有带更新类型标记的后代节点。最后在setupBlock函数中将所有带更新类型标记的后代节点挂载到Block节点的dynamicChildren属性下,这与官网的对Block的描述相对应。

总结

Vue模板编译的目的是将模板转换成渲染函数,期间会经过解析(parse)、转换(transform)和生成(generate)等操作,最终形成浏览器能够识别的JavaScript代码。
template -> (parse) -> ast -> (transform) => codegenNode -> (generate) -> render
Vue模板编译主要依靠vue-compiler完成,将模板转换成渲染函数的目的是生成vnode,以便后续的vue-runtime能够动态更改视图(DOM)。