Vue3.0源码解读 - 编译系统

    技术2026-03-06  8

    一、baseCompile

    baseCompile: 编译器的总入口,是编译器的一个基础骨架(概念上可以理解为基类),然后不同平台的编译系统都是基于baseCompile来进行扩展的,如dom编译、服务端渲染编译、sfc,都是基于baseCompile进行了对应平台下处理场景的扩展。

    function baseCompile( template: string | RootNode, options: CompilerOptions = {} ): CodegenResult { // 省略无关代码... // 生成AST节点树 const ast = isString(template) ? baseParse(template, options) : template // 获取节点转换工具集、指令转换工具集 const [nodeTransforms, directiveTransforms] = getBaseTransformPreset( prefixIdentifiers ) // 遍历AST节点树,对上面生成的AST进行指令转换,生成可用节点,同时根据compiler // 传入的配置(如是否做静态节点提升等)对AST节点树进行优化处理,为rootNode及 // 下属每个节点挂载codegenNode transform( ast, extend({}, options, { prefixIdentifiers, nodeTransforms: [ ...nodeTransforms, ...(options.nodeTransforms || []) // user transforms ], directiveTransforms: extend( {}, directiveTransforms, options.directiveTransforms || {} // user transforms ) }) ) // 对转换及优化后的AST进行代码生成 return generate( ast, extend({}, options, { prefixIdentifiers }) ) }

    二、parser模版解析器

    parser的作用就是将我们传入的template string转化为AST节点树,供后面的transform使用。 rootNode: 根节点是一个临时容器,真正在运行时映射成具体内容的是rootNode下的children,说白了rootNode只是个用来存放实际节点的空壳子,假如parse AST节点时template string中是多根节点,那么没有一个抽象出来的根节点就无法表述完整的树结构,这也是为什么vue3.0能够允许多根模版的原因所在。 Position: 包含offset、line、column三个属性,offset记录parser解析到的位置相对原始template string开头的位置,line记录parser解析到的行数,column为列数,因为parse过程中会遇到\n\t\f之类的转义字符。 baseParse: 将template string解析成AST,AST是vue对节点的一种表述形式,和平时JS生成的抽象语法树是两码事。

    export function baseParse( content: string, // 原始的模版字符串 options: ParserOptions = {} ): RootNode { // 获得parser上下文,相当于class实例化的产物,用来存储parser的一些信息 const context = createParserContext(content, options) // 获取parser开始位置 const start = getCursor(context) // 生成AST节点 return createRoot( parseChildren(context, TextModes.DATA, []), // 生成AST子节点 getSelection(context, start) // 获取根节点位置信息、对应string信息:loc ) }

    createParserContext创建出上下文的结构:

    { options: extend({}, defaultParserOptions, options), // parser配置项 // column、line、offset均是相对template string的全局位置信息 column: 1, // parser解析到的列数 line: 1, // 解析到的行数 offset: 0, // 解析到相对于template string开始的位置 originalSource: content, // 初始template string,即用户定义的完整模版字符串 source: content, // parser处理后的最新template string inPre: false, inVPre: false }

    createRoot: 创建一个虚拟根节点容器,根据模版解析出children和对应的位置信息,透传给根节点对象

    function createRoot( children: TemplateChildNode[], loc = locStub ): RootNode { // 生成AST根节点的结构 return { type: NodeTypes.ROOT, // 节点类型 children, // 子节点 helpers: [], components: [], // 组件节点 directives: [], // 指令节点 hoists: [], imports: [], cached: 0, temps: 0, codegenNode: undefined, // 用于后续generate阶段生成vnode创建物料 // 节点在template string中所处的位置,结构{ source, start, end } // source对应节点在模版中对应的string部分,start、end对应节点起始标签 // 对应template string中的位置 loc } }

    createChildren: 创建AST根节点容器的所属子节点,即模版中实际的节点。该方法为parser核心处理逻辑,用于解析一段“完整”的模版串,比如<div><p>test</p></div>,<div>...</div>和<p>...</p>都是“完整”的,因此会递归的执行parseChildren解析子节点,将解析出的子节点插入父节点中。 过程中两个比较重要的解析方法是parseInterpolation(处理插值)、parseElement(解析dom节点),将重点介绍。

    function parseChildren( context: ParserContext, mode: TextModes, // 祖先节点,是一个栈结构,用于维护节点嵌套关系,越靠后的节点在dom树中的层级越深 ancestors: ElementNode[] ): TemplateChildNode[] { // 父节点 const parent = last(ancestors) const ns = parent ? parent.ns : Namespaces.HTML // 存储解析出来的AST子节点 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])) { // '{{' // 解析以‘{{’开头的模版,parseInterpolation为核心方法,下面重点讲解 node = parseInterpolation(context, mode) } else if (mode === TextModes.DATA && s[0] === '<') { // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state if (s.length === 1) { // 错误处理 emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1) } else if (s[1] === '!') { if (startsWith(s, '<!--')) { // 解析注释节点 node = parseComment(context) } else if (startsWith(s, '<!DOCTYPE')) { // Ignore DOCTYPE by a limitation. node = parseBogusComment(context) } else if (startsWith(s, '<![CDATA[')) { if (ns !== Namespaces.HTML) { node = parseCDATA(context, ancestors) } else { // 错误处理,省略 } } else { // 错误处理省略 } } else if (s[1] === '/') { // 解析结束标签错误的逻辑,此处省略 } else if (/[a-z]/i.test(s[1])) { // 解析正常的html开始标签,获得解析到的AST节点 // parseElement是核心方法,下面重点讲解 node = parseElement(context, ancestors) } else if (s[1] === '?') { // 解析错误处理,省略 } else { // 解析错误处理,省略 } } } 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) } } // Whitespace management for more efficient output // (same as v2 whitespace: 'condense') let removedWhitespace = false if (mode !== TextModes.RAWTEXT) { if (!context.inPre) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i] if (node.type === NodeTypes.TEXT) { if (!/[^\t\r\n\f ]/.test(node.content)) { const prev = nodes[i - 1] const next = nodes[i + 1] // If: // - the whitespace is the first or last node, or: // - the whitespace is adjacent to a comment, or: // - the whitespace is between two elements AND contains newline // Then the whitespace is ignored. if ( !prev || !next || prev.type === NodeTypes.COMMENT || next.type === NodeTypes.COMMENT || (prev.type === NodeTypes.ELEMENT && next.type === NodeTypes.ELEMENT && /[\r\n]/.test(node.content)) ) { removedWhitespace = true nodes[i] = null as any } else { // Otherwise, condensed consecutive whitespace inside the text // down to a single space node.content = ' ' } } else { node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ') } } else if (!__DEV__ && node.type === NodeTypes.COMMENT) { // remove comment nodes in prod removedWhitespace = true nodes[i] = null as any } } } else if (parent && context.options.isPreTag(parent.tag)) { // remove leading newline per html spec // https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element const first = nodes[0] if (first && first.type === NodeTypes.TEXT) { first.content = first.content.replace(/^\r?\n/, '') } } } return removedWhitespace ? nodes.filter(Boolean) : nodes }

    那么入参里的这个mode是什么呢,其实就是vue对节点进行了一个分类,大概看一下:

    getTextMode({ tag, ns }: ElementNode): TextModes { // 节点命名空间是html的情况 if (ns === DOMNamespaces.HTML) { if (tag === 'textarea' || tag === 'title') { // textarea和title标签属于RCDATA类型 return TextModes.RCDATA } if (isRawTextContainer(tag)) { // 文本容器,包括style、script、noscript、iframe,归类到RAWTEXT return TextModes.RAWTEXT } } // 非html命名空间的节点归类到DATA,如svg return TextModes.DATA }

    parseElement: 解析dom元素生成AST节点

    function parseElement( context: ParserContext, ancestors: ElementNode[] ): ElementNode | undefined { __TEST__ && assert(/^<[a-z]/i.test(context.source)) // Start tag. const wasInPre = context.inPre const wasInVPre = context.inVPre const parent = last(ancestors) // 父节点 // 解析开始标签生成AST节点 const element = parseTag(context, TagType.Start, parent) const isPreBoundary = context.inPre && !wasInPre const isVPreBoundary = context.inVPre && !wasInVPre // 如果是自我闭合节点或者空标签,直接返回解析出的AST节点 if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { return element } // 根据开始标签解析出的节点入栈,并解析它的子节点,子节点解析完毕后,父节点出栈 ancestors.push(element) const mode = context.options.getTextMode(element, parent) // 递归解析子节点,子节点解析过程中遇到父节点的结束标签,即解析完成并返回解析结果 const children = parseChildren(context, mode, ancestors) ancestors.pop() // 为当前节点注入children子节点 element.children = children // End tag. if (startsWithEndTagOpen(context.source, element.tag)) { parseTag(context, TagType.End, parent) } else { emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start) if (context.source.length === 0 && element.tag.toLowerCase() === 'script') { const first = children[0] if (first && startsWith(first.loc.source, '<!--')) { emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT) } } } element.loc = getSelection(context, element.loc.start) if (isPreBoundary) { context.inPre = false } if (isVPreBoundary) { context.inVPre = false } return element }

    parseTag:

    function parseTag( context: ParserContext, type: TagType, parent: ElementNode | undefined ): ElementNode { // 标签开始<. const start = getCursor(context) // 正则匹配开始 / 结束标签,\/?表示‘/’可有可无,因为此处匹配的是开始或者结束标签 // 所以有的有‘/’有的没有 const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)! // exec返回的数组第一个是正则匹配文本,后面是子表达式匹配的内容,也就是括号里匹配到的 // 内容,此处子表达式匹配到的是除\t\r\n\f />这些字符外的内容,也就是节点的标签名、 // 各种属性集 const tag = match[1] const ns = context.options.getNamespace(tag, parent) // parser解析完开始标签,推进模版字符串,位数为整个开始标签的长度 advanceBy(context, match[0].length) advanceSpaces(context) // save current state in case we need to re-parse attributes with v-pre const cursor = getCursor(context) const currentSource = context.source // 解析属性props,parseAttributes内部主要调用了parseAttribute,下面会讲到 let props = parseAttributes(context, type) // <pre>标签和v-pre指令的解析逻辑,此处省略... // 标签结束/> let isSelfClosing = false if (context.source.length === 0) { emitError(context, ErrorCodes.EOF_IN_TAG) } else { // 判断标签是否自身闭合 isSelfClosing = startsWith(context.source, '/>') if (type === TagType.End && isSelfClosing) { emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS) } // 根据是否是自闭合标签向前推进对应的位数,普通标签>:1位,自闭合标签/>:2位 advanceBy(context, isSelfClosing ? 2 : 1) } let tagType = ElementTypes.ELEMENT const options = context.options if (!context.inVPre && !options.isCustomElement(tag)) { // 是否有由v-is属性(动态组件) const hasVIs = props.some( p => p.type === NodeTypes.DIRECTIVE && p.name === 'is' ) if (options.isNativeTag && !hasVIs) { // 判断是否是html原生标签,如果不是则标记为组件类型的标签 if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT } else if ( hasVIs || isCoreComponent(tag) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || /^[A-Z]/.test(tag) || tag === 'component' ) { // 被解析为组件标签的case tagType = ElementTypes.COMPONENT } // 插槽标签处理 if (tag === 'slot') { tagType = ElementTypes.SLOT } else if ( tag === 'template' && props.some(p => { return ( p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name) ) }) ) { tagType = ElementTypes.TEMPLATE } } // 由tag解析出的AST节点结构 return { type: NodeTypes.ELEMENT, ns, tag, // 标签名 tagType, // 标签类型,表明是原生还是某种组件类型 props, // 属性,解析后的表达式类型(包含属性的类型、名称、值表达式),具体看parseAttribute isSelfClosing, children: [], // 子节点,由外部的parseChildren生成并注入 loc: getSelection(context, start), // 对应模版字符串中的位置信息 codegenNode: undefined // to be created during transform phase } }

    parseAttribute:

    function parseAttribute( context: ParserContext, nameSet: Set<string> ): AttributeNode | DirectiveNode { // Name. const start = getCursor(context) // 匹配属性名称 const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)! const name = match[0] // 保证属性名称唯一性 if (nameSet.has(name)) { emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE) } nameSet.add(name) if (name[0] === '=') { emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME) } { const pattern = /["'<]/g let m: RegExpExecArray | null while ((m = pattern.exec(name))) { emitError( context, ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME, m.index ) } } // 推进属性名称 advanceBy(context, name.length) // 解析属性值 let value: | { content: string // 引号间(不一定在引号里)的原生文本内容 isQuoted: boolean // 值是否包含在引号里 loc: SourceLocation // 对应位置 } | undefined = undefined if (/^[\t\r\n\f ]*=/.test(context.source)) { // 推进‘=’ advanceSpaces(context) advanceBy(context, 1) advanceSpaces(context) // 解析出属性值,结构见value声明处 value = parseAttributeValue(context) if (!value) { emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE) } } const loc = getSelection(context, start) // 指令属性,包括v-、:、@...开头的属性 if (!context.inVPre && /^(v-|:|@|#)/.test(name)) { // 示例:指令v-bind:test用下面正则匹配后结果为:match = [ // 'v-bind:test', // 'bind', // match[1] // 'test', // match[2] // undefined, // index: 0, // input: 'v-bind:test', // groups: undefined // ] const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec( name )! // 确定指令所属类型:bind(e.g. :test)、on(e.g. @click)、slot // 对于全名的指令,如v-{dirName}:test,他的类型是{}里的内容,即on或bind // 简写命令如@click、:test对应的也是on、bind,指令的使用vue用户应该 // 再熟悉不过了吧 const dirName = match[1] || (startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot') // arg代表属性真名 let arg: ExpressionNode | undefined // match[2]对应的就是指令的真名了,e.g. v-bind:{realName} if (match[2]) { const isSlot = dirName === 'slot' const startOffset = name.indexOf(match[2]) const loc = getSelection( context, getNewPosition(context, start, startOffset), getNewPosition( context, start, startOffset + match[2].length + ((isSlot && match[3]) || '').length ) ) let content = match[2] let isStatic = true if (content.startsWith('[')) { isStatic = false if (!content.endsWith(']')) { emitError( context, ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END ) } content = content.substr(1, content.length - 2) } else if (isSlot) { // #1241 special case for v-slot: vuetify relies extensively on slot // names containing dots. v-slot doesn't have any modifiers and Vue 2.x // supports such usage so we are keeping it consistent with 2.x. content += match[3] || '' } arg = { type: NodeTypes.SIMPLE_EXPRESSION, content, // 指令真名 isStatic, // 是否是静态 isConstant: isStatic, // 是否常量 loc } } if (value && value.isQuoted) { const valueLoc = value.loc valueLoc.start.offset++ valueLoc.start.column++ valueLoc.end = advancePositionWithClone(valueLoc.start, value.content) valueLoc.source = valueLoc.source.slice(1, -1) } // 指令属性的结构(v-if、v-for、v-bind、v-once...) return { type: NodeTypes.DIRECTIVE, // 表明是指令型属性 name: dirName, // 指令类型 exp: value && { type: NodeTypes.SIMPLE_EXPRESSION, content: value.content, isStatic: false, // transformExpression时可能会将isContant置为true,因为有些表达式是不会变化的 // 所以有必要让其参与到静态提升中 isConstant: false, loc: value.loc }, arg, // 指令真名表达式 modifiers: match[3] ? match[3].substr(1).split('.') : [], loc // 位置信息 } } // 普通dom属性的结构,值一定是静态的,不会发生变化,如test="test" return { type: NodeTypes.ATTRIBUTE, // 非指令型指令类型 name, // 属性名称 value: value && { type: NodeTypes.TEXT, content: value.content, loc: value.loc }, // 属性值信息 loc } }

    isEnd: 根据节点栈(ancestors)中的最后一个入栈节点和匹配到的结束标签做比较,如果判断为同一标签,表明节点是合法闭合的

    function isEnd( context: ParserContext, mode: TextModes, ancestors: ElementNode[] ): boolean { const s = context.source switch (mode) { // 非html命名空间节点 case TextModes.DATA: if (startsWith(s, '</')) { //TODO: probably bad performance for (let i = ancestors.length - 1; i >= 0; --i) { if (startsWithEndTagOpen(s, ancestors[i].tag)) { return true } } } break // 直接装载文本的节点:一定是无层级的,直接判断入栈的最后一个节点即可 case TextModes.RCDATA: // title、textarea case TextModes.RAWTEXT: { // script、noscript、iframe、style const parent = last(ancestors) if (parent && startsWithEndTagOpen(s, parent.tag)) { return true } break } case TextModes.CDATA: if (startsWith(s, ']]>')) { return true } break } return !s }

    advanceBy: parser推进template string后,裁切template string为推进后的位置至尾部之间的内容,将其作为最新template string

    function advanceBy(context: ParserContext, numberOfCharacters: number): void { const { source } = context advancePositionWithMutation(context, source, numberOfCharacters) context.source = source.slice(numberOfCharacters) }

    advancePositionWithMutation: 根据传入长度对模版字符串向前推进,同时推进后的最新位置信息。函数在parser中会频繁调用,考虑到拷贝新的位置信息耗费性能,因此直接修改源位置信息以节省开销。

    export function advancePositionWithMutation( pos: Position, // 解析器当前位置信息 source: string, // 当前待分析模版字符串 numberOfCharacters: number = source.length // 模版字符串推进长度 ): Position { let linesCount = 0 // 换行后的总行数 let lastNewLinePos = -1 // 换行后上一行最后一个字符在template string中的位置 // 遍历推进的内容,遇到换行符累加行数linesCount,同时记录上一行最后一个字符的 // 位置lastNewLinePos for (let i = 0; i < numberOfCharacters; i++) { if (source.charCodeAt(i) === 10 /* newline char code */) { linesCount++ lastNewLinePos = i } } // 累加之前的position结果,计算出parser最新的位置信息 pos.offset += numberOfCharacters pos.line += linesCount pos.column = lastNewLinePos === -1 ? pos.column + numberOfCharacters : numberOfCharacters - lastNewLinePos return pos }

    三、transform AST转换器

    transform阶段主要是对parse阶段生成的AST节点树进行转化处理,产出可以在generate阶段生成运行时代码的信息(gencodeNode),转化过程中大量用表达式对象来描述信息(gencodeNode本身也可以是一个表达式对象),下面的代码就能体会到了。 表达式对象有很多种,以最简单的简单表达式(SimpleExpression)为例(比如一个变量引用):

    { type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型标示 loc, // 位置信息 isConstant, // 是否是常量 content, // 表达式内容 // 是否是静态的, // e.g. v-bind:attr="value",value如果是动态变化的变量 // v-bind:attr="true",true是常量不会变化,因此是静态的 isStatic } // 比如v-bind:attr="true",true转换为简单表达式对象就是 // { isContant: true, content: 'true', isStatic: true ... }

    transform:

    function transform(root: RootNode, options: TransformOptions) { // 创建transform上下文 const context = createTransformContext(root, options) // 深度遍历AST,根据节点中的指令(v-if、v-for...转换为相应节点) // 遍历过程中转换完成后每个节点的codegenNode会挂载转换后的节点内容 traverseNode(root, context) if (options.hoistStatic) { // 如果编译器配置了静态节点提升,对静态节点进行提升 hoistStatic(root, context) } if (!options.ssr) { // 根据根节点下的children生成rootNode上的codegenNode createRootCodegen(root, context) } // 将transform上下文上的信息挂载到根节点 root.helpers = [...context.helpers] root.components = [...context.components] root.directives = [...context.directives] root.imports = [...context.imports] root.hoists = context.hoists root.temps = context.temps root.cached = context.cached }

    traverseNode:

    遍历AST节点树过程中,通过node转换器(nodeTransforms)对当前节点进行node转换,子节点全部遍历完成后执行对应指令的onExit回调退出转换。对v-if、v-for等指令的转换生成对应节点,都是由nodeTransforms中对应的指令转换工具完成的。 经nodeTransforms处理过的AST节点会被挂载codeGenNode属性(其实就是调用vnode创建的interface),该属性包含patchFlag等在AST解析阶段无法获得的信息,其作用就是为了在后面的generate阶段生成vnode的创建调用。 本质上codegenNode是一个表达式对象。

    function traverseNode( node: RootNode | TemplateChildNode, context: TransformContext ) { // 上下文记录当前正在遍历的节点 context.currentNode = node // 转换器:transformElement、transformExpression、transformText、 // transformSlotOutlet... // transformElement负责整个节点层面的转换,transformExpression负责 // 节点中表达式的转化,transformText负责节点中文本的转换,转换后会增加 // 一堆表达式表述对象 const { nodeTransforms } = context const exitFns = [] // 依次调用指令转换工具 for (let i = 0; i < nodeTransforms.length; i++) { // 转换器只负责生成onExit回调(具体可以看下面的transformElement), // onExit函数才是执行转换主逻辑的地方,为什么要推到栈中先不执行呢? // 因为要等到子节点都转换完成挂载gencodeNode后,也就是深度遍历完成后 // 再执行当前节点栈中的onExit,这样保证了子节点的表达式全部生成完毕 const onExit = nodeTransforms[i](node, context) if (onExit) { if (isArray(onExit)) { // v-if、v-for为结构化指令,其onExit是数组形式 exitFns.push(...onExit) } else { exitFns.push(onExit) } } if (!context.currentNode) { // node was removed return } else { // node may have been replaced node = context.currentNode } } switch (node.type) { case NodeTypes.COMMENT: if (!context.ssr) { // inject import for the Comment symbol, which is needed for creating // comment nodes with `createVNode` context.helper(CREATE_COMMENT) } break case NodeTypes.INTERPOLATION: // no need to traverse, but we need to inject toString helper if (!context.ssr) { context.helper(TO_DISPLAY_STRING) } break // for container types, further traverse downwards case NodeTypes.IF: // 对v-if生成的节点束进行遍历 for (let i = 0; i < node.branches.length; i++) { traverseNode(node.branches[i], context) } break case NodeTypes.IF_BRANCH: case NodeTypes.FOR: case NodeTypes.ELEMENT: case NodeTypes.ROOT: // 遍历子节点 traverseChildren(node, context) break } // 当前节点树遍历完成,依次执行栈中的指令退出回调onExit let i = exitFns.length while (i--) { exitFns[i]() } }

    transformElement: nodeTransform有很多种,如文本、表达式相关转换,此处仅对element进行讲解,其他类型的有兴趣可查阅源码。 transformElement对原生dom元素和组件类型的AST生成对应的VNODE_CALL接口,即gencodeNode,用于后续generate阶段中进行创建vnode调用。

    const transformElement: NodeTransform = (node, context) => { // 组件和原生dom的type都属于element,tagType标示了具体的类型所属 if ( !( node.type === NodeTypes.ELEMENT && (node.tagType === ElementTypes.ELEMENT || node.tagType === ElementTypes.COMPONENT) ) ) { return } // 返回闭包,也是真正执行transform逻辑生成gencodeNode的地方,返回闭包是为了 // 透出到外部,由外部控制调用时机,等待子代表达式转换生成完毕后再执行当前AST节点 // 转换 return function postTransformElement() { const { tag, props /* parsed props */ } = node // 是否为组件 const isComponent = node.tagType === ElementTypes.COMPONENT // 决定创建组件vnode时的tag值 const vnodeTag = isComponent ? resolveComponentType(node as ComponentNode, context) : `"${tag}"` // 是否是动态组件,resolveComponentType生成的tag值为call表达式 const isDynamicComponent = isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT let vnodeProps: VNodeCall['props'] let vnodeChildren: VNodeCall['children'] let vnodePatchFlag: VNodeCall['patchFlag'] let patchFlag: number = 0 let vnodeDynamicProps: VNodeCall['dynamicProps'] let dynamicPropNames: string[] | undefined let vnodeDirectives: VNodeCall['directives'] // 需要创建block的情况:动态组件(v-bind:is)、svg、动态key... let shouldUseBlock = // dynamic component may resolve to plain elements isDynamicComponent || (!isComponent && // <svg> and <foreignObject> must be forced into blocks so that block // updates inside get proper isSVG flag at runtime. (#639, #643) // This is technically web-specific, but splitting the logic out of core // leads to too much unnecessary complexity. (tag === 'svg' || tag === 'foreignObject' || // #938: elements with dynamic keys should be forced into blocks findProp(node, 'key', true))) // props转化 if (props.length > 0) { // buildProps会在处理props过程中解析出vnode创建所需的表达式对象 // 包括属性、patchflag、动态属性名称集合、指令集合 // buildProps下面会重点介绍 const propsBuildResult = buildProps(node, context) vnodeProps = propsBuildResult.props // ObjectExpressions属性集 patchFlag = propsBuildResult.patchFlag // patchFlag dynamicPropNames = propsBuildResult.dynamicPropNames // 动态属性名称集合 const directives = propsBuildResult.directives // 运行时指令,创建运行时指令的ArrayExpression vnodeDirectives = directives && directives.length ? (createArrayExpression( directives.map(dir => buildDirectiveArgs(dir, context)) ) as DirectiveArguments) : undefined } // 对子节点children进行转化处理 if (node.children.length > 0) { // keep-alive处理逻辑省略... const shouldBuildAsSlots = isComponent && // Teleport is not a real component and has dedicated runtime handling vnodeTag !== TELEPORT && // explained above. vnodeTag !== KEEP_ALIVE if (shouldBuildAsSlots) { const { slots, hasDynamicSlots } = buildSlots(node, context) vnodeChildren = slots if (hasDynamicSlots) { patchFlag |= PatchFlags.DYNAMIC_SLOTS } } else if (node.children.length === 1 && vnodeTag !== TELEPORT) { // 仅有一个子节点且不是teleport类型的情况 const child = node.children[0] const type = child.type // 检查是否包含动态文本节点,即插值、复合表达式 const hasDynamicTextChild = type === NodeTypes.INTERPOLATION || type === NodeTypes.COMPOUND_EXPRESSION if (hasDynamicTextChild && !getStaticType(child)) { patchFlag |= PatchFlags.TEXT // 标记为动态文本 } // 如果当前唯一的子节点是文本节点(插值类型、复合表达式类型、原生文本节点) // 直接讲该文本节点作为vnodeChildren if (hasDynamicTextChild || type === NodeTypes.TEXT) { vnodeChildren = child as TemplateTextChildNode } else { vnodeChildren = node.children } } else { // 多子节点情况直接拷贝其子节点作为vnodeChildren vnodeChildren = node.children } } // patchFlag & dynamicPropNames if (patchFlag !== 0) { if (__DEV__) { // 省略无关代码... } else { vnodePatchFlag = String(patchFlag) } if (dynamicPropNames && dynamicPropNames.length) { // 动态属性字符串化 vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames) } } // 最终生成的VNodeCall结构,和createVnode入参一样的名称,相信大家都熟悉 node.codegenNode = createVNodeCall( context, vnodeTag, vnodeProps, vnodeChildren, vnodePatchFlag, vnodeDynamicProps, vnodeDirectives, !!shouldUseBlock, false /* disableTracking */, node.loc ) } }

    buildProps: 分析属性得到对应的patchFlag信息、动态属性名称、运行时的指令、属性表达式,结构如下:

    { props: PropsExpression | undefined // 属性表达式 directives: DirectiveNode[] 运行时指令 patchFlag: number dynamicPropNames: string[] // 动态属性集 } function buildProps( node: ElementNode, context: TransformContext, props: ElementNode['props'] = node.props, ssr = false ): { props: PropsExpression | undefined directives: DirectiveNode[] patchFlag: number dynamicPropNames: string[] } { const { tag, loc: elementLoc } = node // 节点是否是组件 const isComponent = node.tagType === ElementTypes.COMPONENT // 存储属性表达式(key-value expression) let properties: ObjectExpression['properties'] = [] const mergeArgs: PropsExpression[] = [] // 运行时指令,如自定义指令(v-custom) const runtimeDirectives: DirectiveNode[] = [] // patchFlag analysis let patchFlag = 0 // 解析生成的patchFlag let hasRef = false // 是否包含ref属性(ref节点不能静态提升) let hasClassBinding = false // 含有动态class属性(:class="") let hasStyleBinding = false // 含有动态样式(:style="") let hasHydrationEventBinding = false // ssr let hasDynamicKeys = false // 含有除ref、class、style外的动态属性 const dynamicPropNames: string[] = [] // 解析生成patchFlag const analyzePatchFlag = ({ key/* 属性真名表达式 */, value /* 属性值表达式 */ }: Property) => { if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { // 值属性名称是静态的 const name = key.content // 服务端渲染逻辑,代码省略... if ( value.type === NodeTypes.JS_CACHE_EXPRESSION || ((value.type === NodeTypes.SIMPLE_EXPRESSION || value.type === NodeTypes.COMPOUND_EXPRESSION) && getStaticType(value) > 0) ) { // 属性包含缓存handler或为常量,不做patchFlag分类 return } // 走到此处署名属性值为动态(或ref),根据属性名称分类 if (name === 'ref') { // 属性为ref hasRef = true } else if (name === 'class' && !isComponent) { // 属性为动态class hasClassBinding = true } else if (name === 'style' && !isComponent) { // 属性为动态style hasStyleBinding = true } else if (name !== 'key' && !dynamicPropNames.includes(name)) { // 除key属性外的动态属性,收集到dynamicPropNames数组中 dynamicPropNames.push(name) } } else { // 属性真名为动态的,标示含有动态指令名 hasDynamicKeys = true } } // 对props进行遍历,依次处理 for (let i = 0; i < props.length; i++) { const prop = props[i] if (prop.type === NodeTypes.ATTRIBUTE) { // 静态属性处理 const { loc, name /* 属性名 */, value /* 属性值 */ } = prop if (name === 'ref') { hasRef = true } // 跳过动态is属性,代码省略... // 收集key-value属性表达式 properties.push( // 生成key-value形式的属性表达式,只不过key、value均是表达式类型 createObjectProperty( // key表达式 createSimpleExpression( name, true, getInnerRange(loc, 0, name.length) ), // value表达式 createSimpleExpression( value ? value.content : '', true, value ? value.loc : loc ) ) ) } else { // 指令属性处理 const { name /* 类型 */, arg /* 属性真名表达式 */, exp /* 属性值表达式 */, loc } = prop const isBind = name === 'bind' // : const isOn = name === 'on' // @ // 跳过v-slot、v-once、v-is、ssr下的v-on处理,代码省略... // 特殊case,没有真名的指令,如v-bind="test"、v-on="test" if (!arg && (isBind || isOn)) { // 算包含动态属性的场景 hasDynamicKeys = true if (exp) { if (properties.length) { mergeArgs.push( createObjectExpression(dedupeProperties(properties), elementLoc) ) properties = [] } if (isBind) { mergeArgs.push(exp) } else { // v-on="obj" -> toHandlers(obj) mergeArgs.push({ type: NodeTypes.JS_CALL_EXPRESSION, loc, callee: context.helper(TO_HANDLERS), arguments: [exp] }) } } else { // 错误处理,省略... } continue } // 获取指令对应的转换器函数 const directiveTransform = context.directiveTransforms[name] if (directiveTransform) { // vue内置指令(v-if、v-for...)转换,后面会以v-bind为例 // 讲一下对应的directiveTransform(transformBind) const { // key-value属性表达式数组,通常数组中只有指令对应的一个key-value表达式 props, // 属性是否为运行时指令 needRuntime } = directiveTransform(prop, node, context) // 分析指令prop对应的patchFlag !ssr && props.forEach(analyzePatchFlag) properties.push(...props) // 当前指令为运行时指令 if (needRuntime) { runtimeDirectives.push(prop) if (isSymbol(needRuntime)) { directiveImportMap.set(prop, needRuntime) } } } else { // 指令找不到对应的转换器函数,说明是自定义指令,推到runtimeDirectives中 runtimeDirectives.push(prop) } } } let propsExpression: PropsExpression | undefined = undefined if (mergeArgs.length) { //v-bind="object" v-on="object"边界场景处理 if (properties.length) { mergeArgs.push( createObjectExpression(dedupeProperties(properties), elementLoc) ) } if (mergeArgs.length > 1) { propsExpression = createCallExpression( context.helper(MERGE_PROPS), mergeArgs, elementLoc ) } else { // single v-bind with nothing else - no need for a mergeProps call propsExpression = mergeArgs[0] } } else if (properties.length) { // 正常props场景处理,生成对应的Object表达式对象, propsExpression = createObjectExpression( // dedupeProperties会对重复属性的值进行值的合并,变为一个属性 // 合并后属性value变为ArrayExpression,比如: // 节点上写了多个class,dedupeProperties会把这个class的值 // 合并到一个ArrayExpression里,作为一个属性来处理 dedupeProperties(properties), elementLoc ) } // 根据具体情况生成对应的patchFlag if (hasDynamicKeys) { // 含有动态属性名称 patchFlag |= PatchFlags.FULL_PROPS } else { if (hasClassBinding) { // 有动态class patchFlag |= PatchFlags.CLASS } if (hasStyleBinding) { // 有动态style patchFlag |= PatchFlags.STYLE } if (dynamicPropNames.length) { patchFlag |= PatchFlags.PROPS } if (hasHydrationEventBinding) { patchFlag |= PatchFlags.HYDRATE_EVENTS } } if ( (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && (hasRef || runtimeDirectives.length > 0) ) { patchFlag |= PatchFlags.NEED_PATCH } return { // parse阶段属性集转化后的属性集,是ObjectExpression类型,该表达式由key-value // 属性表达式构成 props: propsExpression, // 运行时的指令,编译阶段不做转换,仍然是parse阶段的值 directives: runtimeDirectives, patchFlag, dynamicPropNames // 动态属性名集合 } }

    transformBind: 绑定类型的指令属性转换,输入parse props,输出key-value属性表达式数组,key、value分别对应属性的名称与值

    const transformBind: DirectiveTransform = (dir/* parse生成的单个prop表达式对象 */, node, context) => { const { exp /* 属性值表达式 */, modifiers /* 修饰符 */, loc } = dir const arg = dir.arg! // 属性真名表达式 if (!exp || (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content)) { context.onError(createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc)) } // 修饰符处理逻辑,省略... return { props: [ // 创建key-value表达式对象,key是属性真名表达式,value是属性值表达式 createObjectProperty(arg!, exp || createSimpleExpression('', true, loc)) ] } }

    traverChildren:

    function traverseChildren( parent: ParentNode, context: TransformContext ) { let i = 0 const nodeRemoved = () => { i-- } for (; i < parent.children.length; i++) { const child = parent.children[i] if (isString(child)) continue // 上下文记录节点间亲子关系 context.parent = parent // 记录当前父节点正在访问的子节点index context.childIndex = i context.onNodeRemoved = nodeRemoved // 深度递归遍历 traverseNode(child, context) } }

    hoistStatic: hoistStatic内部实际调用的是walk

    // transformContext上挂载的hoist方法,exp接受的是AST节点上的codegenNode属性, // codegenNode其实本质上就是一个表达式对象 // hoist方法将原始gencodeNode推入上下文,并生成新的简单表达式对象gencodeNode, // 该对象刻画提升后的变量标识符,并由外部将新gencodeNode挂载到原节点上 fucntion hoist(exp) { // 将codegenNode(exp)推入hoists数组,生成render时会添加到闭包外 context.hoists.push(exp) // 生成简单表达式类型的标识符(vue的表达式系统包括很多种:简单表达式、符合表达式、 // 函数表达式、call等等) // 该标识符其实就是在render函数里声明的变量标识符,如: // const _hoisted_1 = createVNode('div', {}) const identifier = createSimpleExpression( `_hoisted_${context.hoists.length}`, // content:表达式内容,通常为变量名称 false, // isStatic:是否是静态内容,动态会受外界变化的影响 exp.loc, // loc:位置信息 true // isConstant:标示是否是常量 ) identifier.hoisted = exp return identifier } // 遍历AST节点树查找需要做静态提升的内容 function walk( node: ParentNode, context: TransformContext, resultCache: Map<TemplateChildNode, StaticType>, // 这里需要注意下,如果模版中根节点的children只有一个节点,是不需要进行静态提升的 doNotHoistNode: boolean = false ) { // 标记是否含有提升节点 let hasHoistedNode = false // Some transforms, e.g. trasnformAssetUrls from @vue/compiler-sfc, replaces // static bindings with expressions. These expressions are guaranteed to be // constant so they are still eligible for hoisting, but they are only // available at runtime and therefore cannot be evaluated ahead of time. // This is only a concern for pre-stringification (via transformHoist by // @vue/compiler-dom), but doing it here allows us to perform only one full // walk of the AST and allow `stringifyStatic` to stop walking as soon as its // stringficiation threshold is met. // 标记是否含有运行时常量,比如这种case: // <div :class="`myClass`">{{ `test` }}</div> // class属性和插值虽然看似是动态的,但是其内容一直都不变 // 和<div class="myClass">test</div>完全等效,这就是 // 运行时常量,同样参与静态提升 let hasRuntimeConstant = false const { children } = node for (let i = 0; i < children.length; i++) { const child = children[i] // 只有原生dom节点才会做静态提升,比如一些指令类型的节点(if、for)就不会做提升, // 而是作为block if ( child.type === NodeTypes.ELEMENT && child.tagType === ElementTypes.ELEMENT ) { // 处理静态节点提升的逻辑 let staticType if ( !doNotHoistNode && // staticType为0表示不是静态节点,getStaticType会递归判断子树 // 中是否有非静态内容,如果子树中全部节点均为静态,则整颗子树提升 (staticType = getStaticType(child, resultCache)) > 0 ) { if (staticType === StaticType.HAS_RUNTIME_CONSTANT) { hasRuntimeConstant = true } // 在codegenNode打上patchFlag,标示当前节点树为静态提升, // 运行时diff遇到静态节点将直接跳过 ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) // 把codegenNode推入context.hoists中,供生成render函数时使用, // 对应关系:静态节点提升 <-> 当前模版字符串相对应的context // 并将codegenNode替换成新生成的codegenNode // 注意:这里context.hoists中存储的是原始的codegenNode, // 因为存储时的指针是旧指针 child.codegenNode = context.hoist(child.codegenNode!) // 此时child.codegenNode已指向新生成的identifier对象, // child.codegenNode的指针发生了变化 hasHoistedNode = true continue } else { // 非静态节点,staticType为0,但是属性可能存在静态值,也需要做提升处理 const codegenNode = child.codegenNode! if (codegenNode.type === NodeTypes.VNODE_CALL) { const flag = getPatchFlag(codegenNode) if ( (!flag || flag === PatchFlags.NEED_PATCH || flag === PatchFlags.TEXT) && // 这里需要注意一点,属性包含动态变化的key,或者含有ref属性时,节点不会被 // 提升。原因如下: // 1⃣️动态key:动态变化的key表明该节点在diff时可能会被完全替换,顺便提下diff // 的一个点,vue3.0里节点的tag和key有一个不同就会被当作不同节点,从而 // 完全替换掉。 // 2⃣️ref:ref属性是因为即使写了一个形如`<div ref="test"></div>`的 // 节点,但是假如你在setup里声明了有效的同名响应式数据,比如也叫test // 那么静态ref也会和这个响应数据关联起来 !hasDynamicKeyOrRef(child) && // 不能有缓存的属性 !hasCachedProps(child) ) { // 上面的判断表示节点本身不包含children的情况下是静态的,即属性全部为静态 const props = getNodeProps(child) if (props) { // 将节点的props提升到context中 codegenNode.props = context.hoist(props) } } } } } else if (child.type === NodeTypes.TEXT_CALL) { const staticType = getStaticType(child.content, resultCache) if (staticType > 0) { if (staticType === StaticType.HAS_RUNTIME_CONSTANT) { hasRuntimeConstant = true } child.codegenNode = context.hoist(child.codegenNode) hasHoistedNode = true } } // 对子节点递归执行walk解析出静态提升内容 if (child.type === NodeTypes.ELEMENT) { walk(child, context, resultCache) } else if (child.type === NodeTypes.FOR) { // 不提升v-for生成的单个节点,因为要把它作为一个block walk(child, context, resultCache, child.children.length === 1) } else if (child.type === NodeTypes.IF) { for (let i = 0; i < child.branches.length; i++) { // 不提升v-if生成的单个分支节点,因为要把它作为一个block walk( child.branches[i], context, resultCache, child.branches[i].children.length === 1 ) } } } if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) { context.transformHoist(children, context, node) } }

    createRootCodegen: 根据子节点的情况创建根节点的codegenNode,对于子节点为单节点的情况,需要创建block,多节点创建fragment block。 为什么根节点要创建block呢,原因是需要在根级block上挂载动态子代节点(dynamicChildren),在patch阶段做diff操作时忽略dom树层级,减少不必要的遍历成本,而rootNode是与组件template强相关的,这样就保证了diff一个组件时,会直接比较组件根block下的dynamicChildren,有效减少了组件这颗子树的遍历层级,最理想的情况下可以完全忽略根节点以下子树的遍历,想象是不是就酸爽了。

    function createRootCodegen(root: RootNode, context: TransformContext) { const { helper } = context const { children } = root const child = children[0] if (children.length === 1) { // 如果children为单节点,将它转化为block if (isSingleElementRoot(root, child) && child.codegenNode) { // single element root is never hoisted so codegenNode will never be // SimpleExpressionNode // 单根节点不会被提升,而是作为block const codegenNode = child.codegenNode // VNODE_CALL表示codegen是要生成vnode的 if (codegenNode.type === NodeTypes.VNODE_CALL) { codegenNode.isBlock = true // 推入openBlock、createBlock方法,后面generate生成创建代码要用到 helper(OPEN_BLOCK) helper(CREATE_BLOCK) } root.codegenNode = codegenNode } else { // - single <slot/>, IfNode, ForNode: already blocks. // - single text node: always patched. // root codegen falls through via genNode() root.codegenNode = child } } else if (children.length > 1) { // 多根节点创建稳定fragment的block,并打上PATCH_FLAG // 多根节点无容器,因此需要创建fragment VNodeCall root.codegenNode = createVNodeCall( context, helper(FRAGMENT), undefined, root.children, `${PatchFlags.STABLE_FRAGMENT} /* ${ PatchFlagNames[PatchFlags.STABLE_FRAGMENT] } */`, undefined, undefined, true ) } else { // no children = noop. codegen will return null. } }

    四、generate

    codegenNode: 用于生成vnode,启用各种创建vnode的方法调用(createVnode、createBlock等),也就是我们最终在导入组件对象时里面的render函数,文末有关于compiler生成的render函数样例。 CodegenNode是一个复合类型,可以是TemplateChildNode也可以是JSChildNode,前者其实就是我们前面生成的AST节点,后者是表达式对象(如simpleExpression等)。 generate:

    function generate( ast: RootNode, options: CodegenOptions = {} ): CodegenResult { // 创建生成器上下文 const context = createCodegenContext(ast, options) const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context const hasHelpers = ast.helpers.length > 0 const useWithBlock = !prefixIdentifiers && mode !== 'module' const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' // 生成执行函数的前序部分,主要是方法(如createVnode、createBlock等)的引入, // 和静态提升节点的声明 if (!__BROWSER__ && mode === 'module') { genModulePreamble(ast, context, genScopeId) } else { // 浏览器环境生成函数的前序代码 genFunctionPreamble(ast, context) } // enter render function if (!ssr) { // 非浏览器环境推入render函数 if (genScopeId) { push(`const render = ${PURE_ANNOTATION}_withId(`) } push(`function render(_ctx, _cache) {`) } else { if (genScopeId) { push(`const ssrRender = ${PURE_ANNOTATION}_withId(`) } push(`function ssrRender(_ctx, _push, _parent, _attrs) {`) } indent() if (useWithBlock) { // 如果判断需要使用with函数,会将_ctx声明为with函数体内的上下文,这样 // 在调用属性时会方便很多,比如不用with调用_ctx.test,启用with后直接 // 调用test就可以了,不过with函数因为传入未知作用域context,导致js // 预编译阶段不会提前确定好各个相应声明的所属位置,因此运行时会慢一些, // 因为要花一些时间查找声明所属位置 push(`with (_ctx) {`) indent() // 在with函数体内导入节点创建方法,并重命名,防止和用户自定义属性冲突 if (hasHelpers) { push( `const { ${ast.helpers .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`) .join(', ')} } = _Vue` ) push(`\n`) newline() } } // generate asset resolution statements if (ast.components.length) { genAssets(ast.components, 'component', context) if (ast.directives.length || ast.temps > 0) { newline() } } if (ast.directives.length) { genAssets(ast.directives, 'directive', context) if (ast.temps > 0) { newline() } } if (ast.temps > 0) { push(`let `) for (let i = 0; i < ast.temps; i++) { push(`${i > 0 ? `, ` : ``}_temp${i}`) } } if (ast.components.length || ast.directives.length || ast.temps) { push(`\n`) newline() } // generate the VNode tree expression if (!ssr) { push(`return `) } if (ast.codegenNode) { // 根据rootNode.codegenNode生成创建Vnode的函数调用 genNode(ast.codegenNode, context) } else { push(`null`) } if (useWithBlock) { deindent() push(`}`) } deindent() push(`}`) if (genScopeId) { push(`)`) } return { ast, code: context.code, // SourceMapGenerator does have toJSON() method but it's not in the types map: context.map ? (context.map as any).toJSON() : undefined } }

    genFunctionPreamble: 生成函数的前序部分,主要包括createVnode、createBlock等节点创建方法的引入,和静态节点提升的声明

    function genFunctionPreamble(ast: RootNode, context: CodegenContext) { const { ssr, prefixIdentifiers, push, newline, runtimeModuleName, runtimeGlobalName } = context // vue模块导入 const VueBinding = !__BROWSER__ && ssr ? `require(${JSON.stringify(runtimeModuleName)})` : runtimeGlobalName // 解构重命名alias const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` // Generate const declaration for helpers // In prefix mode, we place the const declaration at top so it's done // only once; But if we not prefixing, we place the declaration inside the // with block so it doesn't incur the `in` check cost for every helper access. // AST节点的helpers主要包含createVnode等节点创建方法名,用于下面的解构批量导入对应方法 if (ast.helpers.length > 0) { if (!__BROWSER__ && prefixIdentifiers) { push( `const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n` ) } else { // "with" mode. // save Vue in a separate variable to avoid collision push(`const _Vue = ${VueBinding}\n`) // in "with" mode, helpers are declared inside the with block to avoid // has check cost, but hoists are lifted out of the function - we need // to provide the helper here. // AST中存在提升的静态节点时,导入静态节点创建的方法们 if (ast.hoists.length) { const staticHelpers = [ CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT, CREATE_STATIC ] .filter(helper => ast.helpers.includes(helper)) .map(aliasHelper) .join(', ') push(`const { ${staticHelpers} } = _Vue\n`) } } } // 从@vue/server-renderer模块导入服务端渲染的对应创建方法 if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) { // ssr guaruntees prefixIdentifier: true push( `const { ${ast.ssrHelpers .map(aliasHelper) .join(', ')} } = require("@vue/server-renderer")\n` ) } // 生成静态节点提升对应的代码 genHoists(ast.hoists, context) newline() // return后接render函数 push(`return `) }

    genHoists: 生成静态节点提升声明,形如const _hoisted_${i + 1} = ...这样的节点创建声明

    function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) { if (!hoists.length) { return } context.pure = true const { push, newline, helper, scopeId, mode } = context const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function' newline() // 非浏览器环境push scopeId相关,省略代码... // 生成被提升静态节点的核心逻辑,具体的创建调用了genNode函数 // genNode是真正创建vnode的地方 hoists.forEach((exp, i) => { if (exp) { push(`const _hoisted_${i + 1} = `) genNode(exp, context) newline() } }) // 非浏览器环境pop scopeId相关,省略代码... context.pure = false }

    genNode: 根据codegenNode生成对应创建代码,根据codegenNode的类型执行对应的代码生成逻辑,重点关注VNODE_CALL类型的节点,我们在render函数中调用createBlock、createVnode的相关代码都是通过VNODE_CALL类型节点来生成的。 注意:genNode并非只用来生成dom节点级别的创建代码,而是面向一切CodegenNode类型的节点,比如可以是transform后的props、dynamicProps等表达式对象,transform生成的信息绝大多数都是以表达式对象的形式来表述的。 比如genVNodeCall,在对节点级别生成创建代码过程中,会对VNodeCall对象中的各表达式信息(比如props等)调用genNode进行代码生成,其实是一个层层递归的生成过程: genNode(节点级别) -> genVNodeCall -> genNode(属性级别)

    // 入参的gencodeNode可能是表达式对象类型,也可能是模版节点类型 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: // 省略无关逻辑... 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: // 生成VNodeCall,最常遇到的就是这种 genVNodeCall(node, context) break case NodeTypes.JS_CALL_EXPRESSION: genCallExpression(node, context) break case NodeTypes.JS_OBJECT_EXPRESSION: genObjectExpression(node, context) break case NodeTypes.JS_ARRAY_EXPRESSION: genArrayExpression(node, context) break case NodeTypes.JS_FUNCTION_EXPRESSION: genFunctionExpression(node, context) break case NodeTypes.JS_CONDITIONAL_EXPRESSION: genConditionalExpression(node, context) break case NodeTypes.JS_CACHE_EXPRESSION: genCacheExpression(node, context) break // 服务端渲染相关逻辑省略... /* istanbul ignore next */ case NodeTypes.IF_BRANCH: // noop break default: // 省略无关代码... } }

    genVNodeCall: 生成类似withDirectives((openBlock(true), createBlock(...args)), directivesObj)这样的代码,即render函数中的Vnode核心创建逻辑代码,和我们手写render类似。

    function genVNodeCall(node: VNodeCall, context: CodegenContext) { const { push, helper, pure } = context const { tag, props, children, patchFlag, dynamicProps, directives, isBlock, disableTracking } = node // 是否有自定义指令 if (directives) { push(helper(WITH_DIRECTIVES) + `(`) } // 是否创建block if (isBlock) { push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `) } if (pure) { push(PURE_ANNOTATION) } // 决定是创建block还是普通vnode push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node) // 将tag、props、children等gencodeNode转化为真正的可渲染node,即我们手写render函数时 // 传入的props、children... genNodeList( // 非空参数数组,tag, props...这个时候还是codegenNode表达式对象 genNullableArgs([tag, props, children, patchFlag, dynamicProps]), context ) push(`)`) if (isBlock) { push(`)`) } if (directives) { push(`, `) genNode(directives, context) push(`)`) } }

    genNodeList: 将createVNode所需的参数(tag、props、children and so on)从gencodeNode形式转化为实际调用时的入参形式。

    function genNodeList( // nodes接受的是tag、props、children这些值对应的表达式对象(gencodeNode) nodes: (string | symbol | CodegenNode | TemplateChildNode[])[], context: CodegenContext, multilines: boolean = false, comma: boolean = true ) { const { push, newline } = context // 将gencodeNode依次生成对应的最终节点,即我们createVnode时的入参 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(', ') } } } }

    五、示例

    对于下面的模版

    <template> <div id="app"> <div id="nav"> <p @click="handleAdd">count: {{ count }}</p> <p>{{ bigCount }}</p> </div> </div> </template>

    编译器生成的render函数如下方代码所示: _hoisted_${I}是被提升的静态属性,由于render函数在编译时是以闭包的形式生成的,因此这些提升的变量声明在闭包外的函数体中。

    function render(_ctx, _cache) { return ( openBlock(), createBlock( "div", _hoisted_1, [createVNode("div", _hoisted_2, [ createVNode("p", { onClick: _cache[1] || (_cache[1] = function ($event) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } return _ctx.handleAdd.apply( _ctx, [$event].concat(args)); }) }, "count: " + toDisplayString(_ctx.count), 1 /* TEXT */ ), createVNode("p", null, toDisplayString(_ctx.bigCount), 1 /* TEXT */ )] )] ) ); }
    Processed: 0.011, SQL: 9