Appearance
codegen 如何将 AST 转换为 render 函数字符串?
代码生成(codegen)是 Vue 编译器的最后一个阶段。
它的任务是:接收在 transform 阶段优化过的 AST(抽象语法树),并将其转换为可执行的 JavaScript render 函数字符串。
transform 阶段负责“分析”和“优化”(在哪里优化),而 codegen 阶段只负责“生成代码”(如何将优化结果写入代码)。
generate:代码生成的入口
generate 函数是 codegen 阶段的入口。它的核心任务是:
- 创建一个
CodegenContext(代码生成上下文)。 - 准备
render函数的“外壳”(如function render(_ctx, _cache) { ... })。 - 调用
genNode来递归地“翻译” AST 的主体。 - 返回最终的代码字符串(
context.code)。
CodegenContext:字符串构建器
CodegenContext 是 codegen 过程中的状态管理器。它持有一个字符串构建器,用于拼接最终的代码。
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' 包中导入的函数,如 createVNode、createTextVNode、toDisplayString 等。
翻译示例:
AST 节点:
{ type: TEXT, content: 'Hello' }genText翻译:context.push(_createTextVNode("Hello"))AST 节点:
{ type: INTERPOLATION, content: 'msg' }genInterpolation翻译:context.push(_toDisplayString(_ctx.msg))_ctx是render函数的第一个参数,代表组件实例。
AST 节点:
{ type: ELEMENT, tag: 'div' }genVNodeCall翻译:context.push(_createElementVNode("div", ...))
generate 函数会在 render 函数的开头,自动生成这些 helpers 的导入(import)或别名(const { ... })语句。
应用优化结果:codegen 如何使用“优化情报”
codegen 最重要的工作,是将 transform 阶段分析出的“优化情报”(patchFlag 和 hoists)写入到最终代码中。
打印 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 */)codegen 将 transform 的“优化情报”传递给了运行时。运行时 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: 8,dynamicProps: ["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 阶段的优化成果(patchFlag 和 hoists)“打印”成可执行的 JavaScript 代码。
