Skip to content

codegen 如何将 AST 转换为 render 函数字符串?

代码生成(codegen)是 Vue 编译器的最后一个阶段。

它的任务是:接收在 transform 阶段优化过的 AST(抽象语法树),并将其转换为可执行的 JavaScript render 函数字符串。

transform 阶段负责“分析”和“优化”(在哪里优化),而 codegen 阶段只负责“生成代码”(如何将优化结果写入代码)。

generate:代码生成的入口

generate 函数是 codegen 阶段的入口。它的核心任务是:

  1. 创建一个 CodegenContext(代码生成上下文)。
  2. 准备 render 函数的“外壳”(如 function render(_ctx, _cache) { ... })。
  3. 调用 genNode 来递归地“翻译” AST 的主体。
  4. 返回最终的代码字符串(context.code)。

CodegenContext:字符串构建器

CodegenContextcodegen 过程中的状态管理器。它持有一个字符串构建器,用于拼接最终的代码。

typescript
// CodegenContext 的核心(简化后)
interface CodegenContext {
  // 最终的代码字符串
  code: string 
  // 缩进级别
  indentLevel: number

  // 核心方法:
  // push(str): 追加字符串
  push(code: string): void 
  // indent(): 增加缩进并换行
  indent(): void  
  // deindent(): 减少缩进并换行
  deindent(): void 
  // newline(): 换行
  newline(): void
}

codegen 的所有工作,就是调用 context.push(...) 来拼凑出最终的 render 函数字符串。

genNode:AST 节点“翻译器”

genNode 函数是 codegen 的“翻译器”。它是一个大型 switch 语句,负责将不同类型的 AST 节点翻译成对应的 JS 代码字符串

typescript
// genNode 的核心逻辑 (简化)
function genNode(node: CodegenNode, context: CodegenContext) {
  switch (node.type) {
    case NodeTypes.VNODE_CALL:
      // 翻译“元素节点”
      genVNodeCall(node, context) 
      break
    case NodeTypes.TEXT:
      // 翻译“文本节点”
      genText(node, context) 
      break
    case NodeTypes.INTERPOLATION:
      // 翻译“插值节点” {{ msg }}
      genInterpolation(node, context) 
      break
    case NodeTypes.SIMPLE_EXPRESSION:
      // 翻译“表达式” (如 'msg', 'count + 1')
      genExpression(node, context)
      break
    case NodeTypes.COMPOUND_EXPRESSION:
      // 翻译“复合表达式” (如 'Hello ' + msg)
      genCompoundExpression(node, context)
      break
    // ... 处理 IF, FOR, COMMENT 等所有其他节点
  }
}

运行时辅助函数 (Runtime Helpers):codegen 的“目标代码”

genNode 在翻译时,不会生成 VNode 对象字面量({ type: ... }),而是会生成对“运行时辅助函数 (Runtime Helpers)”的调用

这些 helpers 是从 'vue' 包中导入的函数,如 createVNodecreateTextVNodetoDisplayString 等。

翻译示例:

  • AST 节点{ type: TEXT, content: 'Hello' }

  • genText 翻译context.push(_createTextVNode("Hello"))

  • AST 节点{ type: INTERPOLATION, content: 'msg' }

  • genInterpolation 翻译context.push(_toDisplayString(_ctx.msg))

    • _ctxrender 函数的第一个参数,代表组件实例。
  • AST 节点{ type: ELEMENT, tag: 'div' }

  • genVNodeCall 翻译context.push(_createElementVNode("div", ...))

generate 函数会在 render 函数的开头,自动生成这些 helpers 的导入(import)或别名(const { ... })语句。

应用优化结果:codegen 如何使用“优化情报”

codegen 最重要的工作,是将 transform 阶段分析出的“优化情报”(patchFlaghoists)写入到最终代码中。

打印 Patch Flags (genVNodeCall)

transform 阶段已将 <div :class="cls"> 节点的 codegenNode 打上了 patchFlag: 2 /* CLASS */

genNode 遇到这个 VNODE_CALL 节点时,genVNodeCall 会被调用:

typescript
// genVNodeCall 负责将 AST 节点的属性“翻译”成函数的“参数”
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper } = context
  const { tag, props, children, patchFlag } = node

  // 1. 翻译:_createElementVNode(
  push(helper(CREATE_ELEMENT_VNODE) + `(`) 
  
  // 2. 翻译 tag, props, children
  genNode(tag, context)
  push(', ')
  genNode(props, context)
  push(', ')
  genNode(children, context)
  push(', ')

  // 3. 【应用优化】
  //    将 transform 计算出的 patchFlag 作为第 4 个参数打印
  push(patchFlag) 
  
  push(')')
}

// 最终打印结果 (示例):
// _createElementVNode("div", { class: _ctx.cls }, null, 2 /* CLASS */)

codegentransform 的“优化情报”传递给了运行时。运行时 diff 算法看到这个 2,就知道只需对比 class 属性。

打印静态提升 (genHoists)

transform 阶段的 cacheStatic 插件,会将 100% 静态的节点(如 <p>hello</p>)收集到 ast.hoists 数组中。

generate 函数在“打印” render 函数之前,会先调用 genHoists

typescript
// genHoists (简化)
function genHoists(hoists: JSChildNode[], context: CodegenContext) {
  const { push, newline } = context
  if (hoists.length) {
    newline()
    // 遍历 hoists 数组,将它们打印为顶层 const 变量
    for (let i = 0; i < hoists.length; i++) {
      push(`const _hoisted_${i + 1} = `)
      genNode(hoists[i], context) // 打印 createStaticVNode("p", null, "hello")
      newline()
    }
  }
}

genNode 后续在 render 函数内部再次遇到这个静态节点时,它会发现这个节点已被提升,于是只打印它的变量名

context.push(_hoisted_1)

总结:一个完整的示例

我们来看一个完整的“翻译”过程:

  • 输入模板:

    html
    <div :id="id">
      <p>static</p>
      {{ msg }}
    </div>
  • transform 阶段分析(概念):

    • <div> 是动态的(PROPS),patchFlag: 8dynamicProps: ["id"]
    • <p> 是静态的,patchFlag: -1 /* HOISTED */,被移入 ast.hoists
    • 是动态的。
  • codegen 阶段生成的 render 函数字符串:

    javascript
    // 1. 导入 helpers
    import { toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    // 2. 打印静态提升
    const _hoisted_1 = /*#__PURE__*/ _createStaticVNode("<p>static</p>", 1)
    
    // 3. 打印 render 函数
    export function render(_ctx, _cache) {
      
      // 4. (由 genVNodeCall 打印)
      return (_openBlock(), _createElementBlock("div", {
        id: _ctx.id
      }, [
        // 5. 打印静态节点的“变量名”
        _hoisted_1, 
        // 6. 打印插值
        _toDisplayString(_ctx.msg) 
      ], 8 /* PROPS */, ["id"])) // 7. 打印“优化情报”!
    }

codegen 是 Vue 编译器的最后阶段。它是一个“翻译器”,不负责“优化”,只负责忠实地将 transform 阶段的优化成果(patchFlaghoists)“打印”成可执行的 JavaScript 代码


微信公众号二维码

Last updated: