一、概念:Codegen 在 Vue 编译流程中的地位
在 Vue 3 的模板编译器(@vue/compiler-core)中,编译流程分为三个阶段:
- Parse(解析) :把模板字符串转为 AST(抽象语法树)。
- Transform(转换) :遍历 AST,标记指令、组件、静态节点等信息。
- Codegen(代码生成) :将优化过的 AST 转化为可执行的渲染函数源码(即
render)。
codegen.ts 模块的核心职责就是完成第 3 步:
把抽象语法树(AST)转化为 JavaScript 渲染函数源码。
它生成的最终代码类似下面这样:
javascript
function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, openBlock: _openBlock } = Vue
return (_openBlock(), _createVNode("div", null, "hello world"))
}
}
二、原理:Codegen 的上下文与核心函数设计
1. CodegenContext:代码生成的运行环境
在 createCodegenContext 函数中,Vue 创建了一个上下文对象,保存了代码生成的所有状态:
typescript
interface CodegenContext {
code: string // 生成的代码字符串
line: number // 当前行号
column: number // 当前列号
indentLevel: number // 缩进层次
push(code: string): void // 添加代码片段
indent(): void // 增加缩进
deindent(): void // 减少缩进
newline(): void // 添加换行
}
👉 设计思路 :
Codegen 的整个过程其实就是「字符串拼接 + 结构控制」的过程。Vue 通过 Context 对象封装了"代码生成的状态",使得生成过程可控且可追踪。
2. 代码生成入口:generate(ast, options)
核心函数 generate() 接受一个 RootNode(即整个模板的 AST 根节点)和编译选项:
javascript
export function generate(ast: RootNode, options: CodegenOptions): CodegenResult
它的内部逻辑大致如下:
- 创建上下文
context; - 生成代码前导部分(
genModulePreamble或genFunctionPreamble); - 写入渲染函数签名;
- 调用
genNode(ast.codegenNode)递归生成主体代码; - 返回完整的
{ code, map }结果。
这与 Babel、Rollup 等代码生成器的理念一致:递归遍历 + 拼接输出。
三、对比:Vue 2.x 的模板编译生成逻辑
Vue 2.x 的模板编译由 compileToFunctions() 完成,生成的代码是字符串拼接版的 with(this){ return _c(...) }。
而 Vue 3.x 的新设计引入了:
- 模块化 AST(NodeTypes)结构;
- Context 管理代码位置;
- SourceMap 支持(基于 source-map-js) ;
- 更细粒度的 helper 引入机制 (如
_createVNode,_openBlock等)。
| 对比项 | Vue 2.x | Vue 3.x |
|---|---|---|
| 模板解析结构 | 自定义 parser | 明确的 AST 节点类型体系 |
| 渲染函数输出 | 字符串拼接 | 结构化 codegen 过程 |
| SourceMap | 无 | ✅ 支持 |
| Helper 引入 | 全局 Vue 对象访问 | 按需导入辅助函数 |
| 目标代码 | with(this) 风格 | 支持 module/function 双模式 |
四、实践:生成一个最简渲染函数
让我们看一个简单的例子,模板如下:
css
<div>{{ msg }}</div>
编译后的核心 AST(简化版)如下:
json
{
"type": 0, // RootNode
"children": [
{
"type": 1, // ELEMENT
"tag": "div",
"children": [
{
"type": 5, // INTERPOLATION
"content": { "type": 4, "content": "msg" }
}
]
}
]
}
生成函数调用流程为:
scss
generate(ast)
↓
createCodegenContext()
↓
genFunctionPreamble()
↓
genVNodeCall()
↓
push("return _createVNode('div', null, _toDisplayString(msg))")
最后输出的代码:
javascript
function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode } = Vue
return _createVNode("div", null, _toDisplayString(msg))
}
}
逐步注释:
with (_ctx):把模板内变量都解析为_ctx.xxx。_toDisplayString(msg):把插值表达式安全地转为字符串。_createVNode("div", ...):创建虚拟节点对象。
五、拓展:SourceMap 与辅助函数生成
1. SourceMap 支持
codegen.ts 内部对 SourceMapGenerator 做了封装(CodegenSourceMapGenerator),
在生成每一段代码时,记录源模板中对应的位置(loc.start 与 loc.end),
从而支持 IDE 中的「反查模板源」。
scss
addMapping(node.loc.start, name)
这让 Vue 的编译结果能精准映射到模板行号,有助于调试和热重载。
2. 辅助函数导入
Vue 会根据 AST 中用到的功能,自动按需引入运行时 helper:
javascript
import { createVNode as _createVNode, toDisplayString as _toDisplayString } from "vue"
这些 helper 在 runtimeHelpers.ts 中定义,用于构建虚拟 DOM、表达式求值等。
六、潜在问题与性能考量
-
性能优化
- Vue 在 codegen 阶段通过
hoistStatic()提前提取静态节点; PURE_ANNOTATION标记配合 Terser 可实现静态折叠。
- Vue 在 codegen 阶段通过
-
安全性问题
- 由于生成代码使用
new Function()执行,因此模板内容需先通过 parse 阶段安全过滤。
- 由于生成代码使用
-
可读性权衡
- 代码生成逻辑极度模块化(数百个 NodeType 分支),可维护性优异但初学门槛高。
-
SourceMap 性能损耗
- 对大型模板启用
sourceMap: true时可能影响编译速度。 - Vue 内部通过
_mappings.add()直接写入以减轻性能开销。
- 对大型模板启用
七、结语
codegen.ts 是 Vue 编译器中最复杂、但也最具系统性的模块之一。
它将 AST 与运行时世界衔接起来,最终产出浏览器可执行的渲染逻辑。
通过这一层,你可以清楚理解从模板到 Virtual DOM 的"最后一跳"。
本文部分内容借助 AI 辅助生成,并由作者整理审核。