主题
实现编译器 - Codegen 代码生成
本节对标 Vue 3 源码
@vue/compiler-core中的codegen.ts源码位置:packages/compiler-core/src/codegen.ts
Codegen 的作用
Codegen(Code Generation,代码生成)是编译器三段式的最后一个阶段。它将 Transform 后的 AST 转换为可执行的 render 函数代码字符串:
Transform 后的 AST
│
▼
┌─────────┐
│ Codegen │
└────┬────┘
│
▼
render 函数代码字符串
// 例如:
// const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue
//
// return function render(_ctx, _cache) {
// return _createElementVNode('div', null, 'hi,' + _toDisplayString(_ctx.message))
// }Codegen 的核心工作:
- 生成 import / 解构语句(导言部分)
- 生成 render 函数签名
- 递归遍历 AST 节点,根据类型生成对应代码
- 处理缩进和格式化
CodegenContext 上下文
ts
// 对标 packages/compiler-core/src/codegen.ts - CodegenContext
// 代码生成上下文接口:管理生成过程中的代码输出和格式化
interface CodegenContext {
code: string // 最终生成的代码字符串
indentLevel: number // 当前缩进层级
push(code: string): void // 向代码字符串追加内容
indent(): void // 增加缩进并换行
deindent(): void // 减少缩进并换行
newline(): void // 换行并应用当前缩进
helper(key: symbol): string // 将辅助函数 symbol 转为带 _ 前缀的变量名
}
// 创建代码生成上下文的工厂函数
function createCodegenContext() : CodegenContext {
const context: CodegenContext = {
code: '', // 初始为空字符串,后续通过 push 不断追加
indentLevel: 0, // 初始缩进为 0
// 向输出的代码字符串追加内容
push(source: string) {
context.code += source
},
// 增加缩进层级并换行(进入代码块时调用)
indent() {
context.indentLevel++
context.newline() // 立即换行并应用新的缩进
},
// 减少缩进层级并换行(退出代码块时调用)
deindent() {
context.indentLevel--
context.newline() // 立即换行并应用新的缩进
},
// 换行并在新行前添加当前缩进级别的空格(每级2个空格)
newline() {
context.code += '\n' + ' '.repeat(context.indentLevel)
},
// 将辅助函数 symbol 转换为代码中使用的变量名
// 例如:TO_DISPLAY_STRING → '_toDisplayString'
helper(key: symbol) {
return `_${helperNameMap[key]}`
},
}
return context
}上下文设计解析
这个上下文的设计非常巧妙:
code:最终生成的代码字符串,通过push不断追加push:所有代码输出都通过这个方法,方便调试和 source map 生成indent/deindent:控制缩进层级,生成格式化的代码newline:换行并添加当前缩进helper:将辅助函数 symbol 转换为带_前缀的变量名
generate 入口函数
ts
// 对标 packages/compiler-core/src/codegen.ts - generate
// 代码生成的入口函数:将转换后的 AST 生成完整的 render 函数代码字符串
export function generate(ast: any) {
const context = createCodegenContext() // 创建代码生成上下文
const { push, indent, deindent, newline } = context // 解构常用方法
// 第一步:生成导言部分(辅助函数的解构声明)
genFunctionPreamble(ast, context)
// 第二步:生成 render 函数签名
const functionName = 'render'
const args = ['_ctx', '_cache'] // render 函数的参数列表
const signature = args.join(', ') // 拼接为 '_ctx, _cache'
push(`function ${functionName}(${signature}) {`) // 输出函数声明头部
indent() // 进入函数体,增加缩进
push('return ') // 输出 return 关键字
// 第三步:生成 render 函数体(返回的 VNode 表达式)
if (ast.codegenNode) {
genNode(ast.codegenNode, context) // 根据 codegenNode 生成代码
} else {
push('null') // 没有 codegenNode 时返回 null
}
deindent() // 退出函数体,减少缩进
push('}') // 输出函数闭合大括号
return {
code: context.code, // 返回最终生成的代码字符串
}
}genFunctionPreamble —— 生成函数导言
ts
// 对标 packages/compiler-core/src/codegen.ts - genFunctionPreamble
// 生成函数导言部分:从 Vue 对象中解构出需要的辅助函数
function genFunctionPreamble(ast: any, context: CodegenContext) {
const { push, newline } = context
// 生成辅助函数的别名映射,如 'toDisplayString: _toDisplayString'
const aliasHelper = (s: symbol) =>
`${helperNameMap[s]}: _${helperNameMap[s]}`
if (ast.helpers.length > 0) {
const VueBinging = 'Vue'
// 生成解构语句:const { toDisplayString: _toDisplayString, ... } = Vue
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`,
)
push('\n')
newline()
// 添加 return 关键字,使整个输出成为一个返回 render 函数的表达式
push('return ')
}
}生成的代码示例:
ts
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue
return function render(_ctx, _cache) {
return _createElementVNode('div', null, 'hi,' + _toDisplayString(_ctx.message))
}导言做了两件事:
- 从
Vue对象中解构出需要的辅助函数,并用_前缀重命名(避免命名冲突) - 添加
return关键字,使整个输出是一个返回 render 函数的表达式
genNode —— 节点分发
ts
// 对标 packages/compiler-core/src/codegen.ts - genNode
// 节点代码生成的分发器:根据 AST 节点类型调用对应的代码生成函数
function genNode(node: any, context: CodegenContext) {
switch (node.type) {
case NodeTypes.TEXT:
genText(node, context) // 生成文本字面量
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context) // 生成 toDisplayString 调用
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context) // 生成表达式代码
break
case NodeTypes.ELEMENT:
// 元素节点委托给其 codegenNode(通常是 VNODE_CALL)来生成代码
genNode(node.codegenNode, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context) // 生成复合表达式(文本 + 插值拼接)
break
case NodeTypes.VNODE_CALL:
genElement(node, context) // 生成 createElementVNode 调用
break
default:
break
}
}genNode 是一个分发器,根据节点类型调用对应的生成函数。注意 ELEMENT 类型会直接委托给其 codegenNode(通常是 VNODE_CALL 类型)。
genText —— 生成文本字面量
ts
// 对标 packages/compiler-core/src/codegen.ts - genText
// 生成文本节点的代码:将文本内容包装为字符串字面量
// 例如:{ content: "hello" } → 'hello'
function genText(node: any, context: CodegenContext) {
context.push(`'${node.content}'`)
}输入:{ type: TEXT, content: "hello" }
输出:'hello'
genInterpolation —— 生成 toDisplayString 调用
ts
// 对标 packages/compiler-core/src/codegen.ts - genInterpolation
// 生成插值表达式的代码:包装为 _toDisplayString(expr) 调用
function genInterpolation(node: any, context: CodegenContext) {
const { push, helper } = context
push(`${helper(TO_DISPLAY_STRING)}(`) // 输出 '_toDisplayString('
genNode(node.content, context) // 递归生成内部表达式的代码
push(')') // 输出闭合括号 ')'
}输入:{ type: INTERPOLATION, content: { type: SIMPLE_EXPRESSION, content: "message" } }
输出:_toDisplayString(message)
toDisplayString 是 Vue 3 运行时提供的辅助函数,作用类似于 String(value),但对 null/undefined 等做了友好处理。
genExpression —— 生成表达式代码
ts
// 对标 packages/compiler-core/src/codegen.ts - genExpression
// 生成简单表达式的代码:直接输出表达式字符串
// 在完整 Vue 3 中,transform 阶段已经将 msg 转为 _ctx.msg,此处原样输出
function genExpression(node: any, context: CodegenContext) {
context.push(node.content) // 直接输出表达式内容,如 'message' 或 '_ctx.msg'
}输入:{ type: SIMPLE_EXPRESSION, content: "message" }
输出:message
在完整 Vue 3 中,expression 经过 transform 后已经被加上了 _ctx. 前缀,所以这里只需要原样输出。
genCompoundExpression —— 处理复合表达式
ts
// 对标 packages/compiler-core/src/codegen.ts - genCompoundExpression
// 生成复合表达式的代码:遍历 children 数组,对字符串直接输出,对节点递归生成
// children 数组格式如:[TextNode, " + ", InterpolationNode, " + ", TextNode]
function genCompoundExpression(node: any, context: CodegenContext) {
const { push } = context
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (typeof child === 'string') {
// 字符串类型:直接输出连接符 " + "
push(child)
} else {
// AST 节点类型(TEXT 或 INTERPOLATION):递归调用 genNode 生成代码
genNode(child, context)
}
}
}复合表达式是 transformText 阶段合并的结果,其 children 数组交替包含 AST 节点和字符串连接符。
输入:
{
type: COMPOUND_EXPRESSION,
children: [
{ type: TEXT, content: "hi," },
" + ",
{ type: INTERPOLATION, content: { type: SIMPLE_EXPRESSION, content: "message" } }
]
}输出:'hi,' + _toDisplayString(message)
genElement —— 生成 createElementVNode 调用
ts
// 对标 packages/compiler-core/src/codegen.ts - genVNodeCall
// 生成元素的代码:输出 _createElementVNode(tag, props, children) 调用
function genElement(node: any, context: CodegenContext) {
const { push, helper } = context
const { tag, props, children } = node
// 输出辅助函数名和左括号
push(`${helper(CREATE_ELEMENT_VNODE)}(`)
// 处理参数列表:genNullable 去除末尾多余的 null,genNodeList 生成逗号分隔的参数
genNodeList(genNullable([tag, props, children]), context)
push(')') // 输出闭合括号
}
// 从参数列表末尾去除 null 值,简化生成的代码
// 例如:['div', null, null] → ['div'](去掉尾部的 null)
// 但保留中间的 null:['div', null, children] → ['div', 'null', children]
function genNullable(args: any[]) {
let i = args.length
// 从末尾向前找到第一个非 null 值
while (i--) {
if (args[i] != null) break
}
// 截取到最后一个非 null 值,并将数组中的 null 值替换为字符串 'null'
return args.slice(0, i + 1).map((arg) => arg || 'null')
}
// 生成参数列表:将节点数组转为逗号分隔的代码
function genNodeList(nodes: any[], context: CodegenContext) {
const { push } = context
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (typeof node === 'string') {
push(node) // 字符串参数直接输出(如标签名 "'div'" 或 "null")
} else {
genNode(node, context) // AST 节点递归生成代码
}
if (i < nodes.length - 1) {
push(', ') // 参数之间用逗号分隔
}
}
}genNullable 的作用是去掉末尾的 null 参数。例如 createElementVNode('div', null, null) 可以简化为 createElementVNode('div')。
输入:
{
type: VNODE_CALL,
tag: "'div'",
props: null,
children: { type: COMPOUND_EXPRESSION, children: [...] }
}输出:_createElementVNode('div', null, 'hi,' + _toDisplayString(message))
生成的 Render 函数结构
完整的代码生成结果:
ts
// 输入模板:<div>hi,{{ message }}</div>
// 生成的代码:
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue
return function render(_ctx, _cache) {
return _createElementVNode('div', null, 'hi,' + _toDisplayString(_ctx.message))
}这段代码的结构:
- 导言:从
Vue对象解构辅助函数 - render 函数:接收
_ctx(组件实例代理)和_cache(缓存) - 返回值:一个
createElementVNode调用,描述 VNode 树
完整实现
ts
import { NodeTypes } from './ast'
import {
CREATE_ELEMENT_VNODE,
TO_DISPLAY_STRING,
helperNameMap,
} from './runtimeHelpers'
// 代码生成入口函数
export function generate(ast: any) {
const context = createCodegenContext()
const { push, indent, deindent, newline } = context
// 生成导言(辅助函数解构声明)
genFunctionPreamble(ast, context)
// 生成 render 函数
push('function render(_ctx, _cache) {')
indent()
push('return ')
// 根据 codegenNode 生成函数体
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push('null') // 空模板返回 null
}
deindent()
push('}')
return { code: context.code }
}
// 创建代码生成上下文
function createCodegenContext() {
const context: any = {
code: '', // 累积的输出代码
indentLevel: 0, // 缩进层级
push(source: string) {
context.code += source // 追加代码
},
indent() {
context.indentLevel++ // 增加缩进
context.newline()
},
deindent() {
context.indentLevel-- // 减少缩进
context.newline()
},
newline() {
// 换行并添加缩进空格
context.code += '\n' + ' '.repeat(context.indentLevel)
},
helper(key: symbol) {
// Symbol → 带前缀的变量名,如 '_toDisplayString'
return `_${helperNameMap[key]}`
},
}
return context
}
// 生成导言:辅助函数解构 + return 关键字
function genFunctionPreamble(ast: any, context: any) {
const { push, newline } = context
// 将 Symbol 映射为 '原名: _原名' 格式的别名
const aliasHelper = (s: symbol) =>
`${helperNameMap[s]}: _${helperNameMap[s]}`
if (ast.helpers.length > 0) {
// 生成:const { toDisplayString: _toDisplayString, ... } = Vue
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = Vue`,
)
push('\n')
newline()
push('return ') // 整个输出是一个返回 render 函数的表达式
}
}
// 节点分发器:根据类型调用对应的代码生成函数
function genNode(node: any, context: any) {
switch (node.type) {
case NodeTypes.TEXT:
genText(node, context) // 文本 → 字符串字面量
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context) // 插值 → toDisplayString 调用
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context) // 表达式 → 原样输出
break
case NodeTypes.ELEMENT:
genNode(node.codegenNode, context) // 元素 → 委托给 codegenNode
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context) // 复合表达式 → 拼接输出
break
case NodeTypes.VNODE_CALL:
genElement(node, context) // VNode 调用 → createElementVNode
break
}
}
// 文本节点:输出为带引号的字符串
function genText(node: any, context: any) {
context.push(`'${node.content}'`)
}
// 插值节点:包装为 _toDisplayString(expr) 调用
function genInterpolation(node: any, context: any) {
const { push, helper } = context
push(`${helper(TO_DISPLAY_STRING)}(`) // 输出函数名和左括号
genNode(node.content, context) // 生成内部表达式
push(')') // 闭合括号
}
// 简单表达式:直接输出表达式字符串
function genExpression(node: any, context: any) {
context.push(node.content)
}
// 复合表达式:交替输出文本/插值节点和 " + " 连接符
function genCompoundExpression(node: any, context: any) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (typeof child === 'string') {
context.push(child) // 连接符 " + "
} else {
genNode(child, context) // AST 节点
}
}
}
// 元素节点:生成 _createElementVNode(tag, props, children) 调用
function genElement(node: any, context: any) {
const { push, helper } = context
const { tag, props, children } = node
push(`${helper(CREATE_ELEMENT_VNODE)}(`) // 输出函数名
genNodeList(genNullable([tag, props, children]), context) // 输出参数列表
push(')')
}
// 去除参数列表末尾多余的 null,保留中间的 null
function genNullable(args: any[]) {
let i = args.length
while (i--) {
if (args[i] != null) break // 找到最后一个非 null 参数
}
return args.slice(0, i + 1).map((arg) => arg || 'null')
}
// 生成逗号分隔的参数列表
function genNodeList(nodes: any[], context: any) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (typeof node === 'string') {
context.push(node) // 字符串参数直接输出
} else {
genNode(node, context) // AST 节点递归生成
}
if (i < nodes.length - 1) {
context.push(', ') // 参数之间加逗号
}
}
}更多代码生成示例
示例 1:纯文本
html
hits
function render(_ctx, _cache) {
return 'hi'
}示例 2:单个插值
html
{{ message }}ts
const { toDisplayString: _toDisplayString } = Vue
return function render(_ctx, _cache) {
return _toDisplayString(_ctx.message)
}示例 3:元素 + 文本
html
<div>hello</div>ts
const { createElementVNode: _createElementVNode } = Vue
return function render(_ctx, _cache) {
return _createElementVNode('div', null, 'hello')
}示例 4:元素 + 复合表达式
html
<div>hi,{{ msg }}</div>ts
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue
return function render(_ctx, _cache) {
return _createElementVNode('div', null, 'hi,' + _toDisplayString(_ctx.msg))
}示例 5:带属性的元素
html
<div id="app" class="container">{{ msg }}</div>ts
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode } = Vue
return function render(_ctx, _cache) {
return _createElementVNode('div', {"id":"app","class":"container"}, _toDisplayString(_ctx.msg))
}对比 React
| 维度 | Vue 3 Codegen | React JSX Compile |
|---|---|---|
| 输出格式 | render 函数字符串 | JSX → jsx() / createElement() 调用 |
| 运行方式 | new Function(code) 生成函数 | 直接编译为 JS 模块 |
| 辅助函数 | toDisplayString / createElementVNode | jsx / jsxs / createElement |
| import 方式 | const { ... } = Vue 运行时解构 | import { jsx } from 'react/jsx-runtime' |
| 运行时编译 | 支持(但不推荐) | 不支持(必须预编译) |
| 格式化 | indent/deindent 自动格式化 | Babel 输出 + prettier |
一个关键区别:Vue 3 的 codegen 生成的是代码字符串,需要通过 new Function() 在运行时转换为函数。而 React 的 JSX 编译(通过 Babel/SWC)直接输出 JS 代码,不需要运行时 new Function。
Vue 支持运行时编译(在浏览器中直接编译模板),这在开发阶段和某些动态场景下非常有用,但在生产环境中建议使用构建时编译以获得更好的性能。
测试用例
ts
import { baseParse, NodeTypes } from '../src/parse'
import { transform } from '../src/transform'
import { generate } from '../src/codegen'
import { transformElement } from '../src/transforms/transformElement'
import { transformText } from '../src/transforms/transformText'
import { transformExpression } from '../src/transforms/transformExpression'
describe('codegen', () => {
// 测试:纯文本应生成返回字符串字面量的 render 函数
it('should generate code for text', () => {
const ast = baseParse('hi')
transform(ast, { nodeTransforms: [transformExpression] })
const { code } = generate(ast)
// 使用 toMatchInlineSnapshot 验证生成代码的精确格式
expect(code).toMatchInlineSnapshot(`
"function render(_ctx, _cache) {
return 'hi'
}"
`)
})
// 测试:插值表达式应生成包含 toDisplayString 调用的代码
it('should generate code for interpolation', () => {
const ast = baseParse('{{ message }}')
transform(ast, { nodeTransforms: [transformExpression] })
const { code } = generate(ast)
// 断言:生成的代码包含辅助函数和表达式变量名
expect(code).toContain('toDisplayString')
expect(code).toContain('message')
})
// 测试:简单元素应生成 createElementVNode 调用
it('should generate code for simple element', () => {
const ast = baseParse('<div>hello</div>')
transform(ast, {
nodeTransforms: [transformExpression, transformElement, transformText],
})
const { code } = generate(ast)
// 断言:代码中包含 createElementVNode、标签名和文本内容
expect(code).toContain('createElementVNode')
expect(code).toContain("'div'")
expect(code).toContain("'hello'")
})
// 测试:元素内包含插值时应生成复合表达式(用 + 连接文本和 toDisplayString)
it('should generate code for element with interpolation', () => {
const ast = baseParse('<div>hi,{{ msg }}</div>')
transform(ast, {
nodeTransforms: [transformExpression, transformElement, transformText],
})
const { code } = generate(ast)
// 断言:代码中同时包含元素创建、字符串转换、文本和连接符
expect(code).toContain('createElementVNode')
expect(code).toContain('toDisplayString')
expect(code).toContain("'hi,'")
expect(code).toContain('msg')
expect(code).toContain(' + ')
})
// 测试:有辅助函数时应生成正确的导言(解构声明)
it('should generate preamble with helpers', () => {
const ast = baseParse('<div>{{ msg }}</div>')
transform(ast, {
nodeTransforms: [transformExpression, transformElement, transformText],
})
const { code } = generate(ast)
// 断言:导言包含 const { ... } = Vue 的解构语句和 return 关键字
expect(code).toContain('const {')
expect(code).toContain('} = Vue')
expect(code).toContain('return function render')
})
// 测试:空模板应生成返回 null 的 render 函数
it('should generate null for empty ast', () => {
const ast = baseParse('')
transform(ast, { nodeTransforms: [] })
ast.codegenNode = undefined // 手动清除 codegenNode 模拟空模板
const { code } = generate(ast)
// 断言:函数体中返回 null
expect(code).toContain('return null')
})
// 测试:文本 + 插值 + 文本的复合表达式应正确拼接
it('should handle text + interpolation + text', () => {
const ast = baseParse('<p>hello {{ name }} world</p>')
transform(ast, {
nodeTransforms: [transformExpression, transformElement, transformText],
})
const { code } = generate(ast)
// 断言:三段内容都出现在生成的代码中
expect(code).toContain("'hello '")
expect(code).toContain('name')
expect(code).toContain("' world'")
})
// 测试:生成的代码是合法的 JavaScript,可以被 new Function 执行
it('should generate complete render function', () => {
const ast = baseParse('<div>hi,{{ message }}</div>')
transform(ast, {
nodeTransforms: [transformExpression, transformElement, transformText],
})
const { code } = generate(ast)
// 断言:生成的代码不会抛出语法错误
expect(() => new Function(code)).not.toThrow()
})
})设计分析
1. 字符串拼接 vs AST → Code
Vue 3 的 codegen 采用了最直接的方式——字符串拼接。虽然看起来"原始",但有以下好处:
- 简单可靠:没有中间表示,减少了出错的可能
- 可读性好:生成的代码格式化良好,便于调试
- Source Map 支持:每次
push都可以记录位置映射
2. push 方法的设计
所有代码输出都通过 push 方法,这不仅是封装,更是为 Source Map 做准备。在 Vue 3 完整实现中,push 方法还会记录 AST 节点的位置信息,用于生成 Source Map:
ts
// Vue 3 完整版的 push 方法,除了追加代码还支持 source map
function push(code: string, node?: any) {
context.code += code
if (node) {
// 如果提供了 AST 节点,记录代码位置与模板位置的映射关系
// 这使得浏览器 DevTools 能直接定位到模板中的位置
context.map?.addMapping(...)
}
}3. genNullable 的尾部裁剪
createElementVNode('div', null, null) 虽然正确,但 null 参数是不必要的。genNullable 从末尾裁剪掉 null 参数,生成更简洁的代码:
ts
// 裁剪前:createElementVNode('div', null, null)
// 裁剪后:createElementVNode('div')
// 但不裁剪中间的 null:createElementVNode('div', null, children)4. 缩进管理
indent/deindent/newline 三个方法协同工作,自动维护代码缩进:
ts
push('function render() {') // function render() {
indent() // (缩进+换行)
push('return ') // return
push('...') // ...
deindent() // (减少缩进+换行)
push('}') // }本节小结
- Codegen 的职责 — 将转换后的 AST 生成 render 函数代码字符串
- CodegenContext — 上下文对象,管理代码输出、缩进、辅助函数名称
- generate — 入口函数,协调导言生成和节点代码生成
- genNode — 分发器,根据节点类型调用不同的代码生成函数
- genElement — 生成
createElementVNode(tag, props, children)调用 - genCompoundExpression — 处理文本+插值的组合表达式
- genFunctionPreamble — 生成导言部分(辅助函数解构)
- genNullable — 尾部裁剪 null 参数,生成更简洁的代码
下一节实现编译器与运行时的联调,完成 template → DOM 的全链路。