上文我们讲到了编译时vue如何转换标准AST中vue独有的属性、指令(例如v-for
、#slot
)等内容。本文我们来分析一下转换后的AST最终如何生成代码。
一、示例模板
让我们以一个具体的模板为例,分析代码生成的完整过程:
vue
<template>
<div class="container">
<h1>{{ title }}</h1>
<button @click="handleClick">Count: {{ count }}</button>
</div>
</template>
二、转换后的 AST 结构
经过解析和转换后,这个模板会生成如下的 AST:
js
const ast = {
type: NodeTypes.ROOT,
children: [
{
type: NodeTypes.ELEMENT,
tag: "div",
props: [
{
type: NodeTypes.ATTRIBUTE,
name: "class",
value: {
type: NodeTypes.TEXT,
content: "container",
},
},
],
children: [
{
type: NodeTypes.ELEMENT,
tag: "h1",
children: [
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "title",
},
},
],
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: '"h1"',
props: null,
children: {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "title",
},
},
patchFlag: PatchFlags.TEXT, // 1 表示动态文本内容
dynamicProps: null,
isBlock: false,
disableTracking: false,
isComponent: false,
},
},
{
type: NodeTypes.ELEMENT,
tag: "button",
props: [
{
type: NodeTypes.DIRECTIVE,
name: "on",
arg: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "click",
isStatic: true,
},
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "handleClick",
},
},
],
children: [
{
type: NodeTypes.TEXT,
content: "Count: ",
},
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "count",
},
},
],
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: '"button"',
props: {
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: "onClick",
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content:
"_cache[0] || (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args))",
isStatic: false,
},
},
],
},
children: [
{
type: NodeTypes.TEXT,
content: "Count: ",
},
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "count",
},
},
],
patchFlag: PatchFlags.TEXT, // 1 表示动态文本内容
dynamicProps: null,
isBlock: false,
disableTracking: false,
isComponent: false,
},
},
],
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: '"div"',
props: {
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: "class",
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "container",
isStatic: true,
},
},
],
},
children: [
// h1 的 codegenNode
// button 的 codegenNode
],
patchFlag: 0, // 0 表示没有动态内容
dynamicProps: null,
isBlock: true,
disableTracking: false,
isComponent: false,
},
},
],
codegenNode: {
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
props: null,
children: [
/* div 的 codegenNode */
],
patchFlag: PatchFlags.STABLE_FRAGMENT,
dynamicProps: null,
isBlock: true,
disableTracking: false,
isComponent: false,
},
helpers: new Set([
CREATE_ELEMENT_VNODE,
TO_DISPLAY_STRING,
CREATE_ELEMENT_BLOCK,
OPEN_BLOCK,
]),
components: [],
directives: [],
hoists: [
{
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: "class",
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "container",
isStatic: true,
},
},
],
},
],
imports: [],
temps: 0,
cached: 1,
};
这个 AST 结构展示了转换后的完整状态,包括:
- 每个元素节点都添加了
codegenNode
,包含生成代码所需的所有信息 - 根节点的
codegenNode
使用 Fragment 包装 - 静态内容(如 class="container")被提升到
hoists
数组 - 事件处理器被缓存(cached 计数为 1)
- 添加了所需的辅助函数(helpers)
- 为动态内容添加了 patch flags 标记
三、AST 生成的最终代码
让我们看看转换后的 AST 结构生成的最终代码
1. 生成的代码
js
// 1. 导入辅助函数
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
// 2. 静态提升的内容
const _hoisted_1 = { class: "container" };
// 3. 渲染函数
export function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock("div", _hoisted_1, [
_createElementVNode(
"h1",
null,
_toDisplayString(_ctx.title),
1 /* TEXT */
),
_createElementVNode(
"button",
{
onClick:
_cache[0] ||
(_cache[0] = (...args) =>
_ctx.handleClick && _ctx.handleClick(...args)),
},
"Count: " + _toDisplayString(_ctx.count),
1 /* TEXT */
),
])
);
}
2. 代码生成说明
-
辅助函数导入:
createElementVNode
:创建普通元素节点toDisplayString
:转换插值表达式内容openBlock
:开启块级作用域createElementBlock
:创建块级元素
-
静态提升:
- 将静态的 class 属性提升为常量
_hoisted_1
- 避免重复创建静态内容
- 将静态的 class 属性提升为常量
-
事件处理:
js
onClick: _cache[0] ||
(_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args));
- 使用事件缓存优化
- 处理可能不存在的方法调用
- 动态内容处理:
js
_toDisplayString(_ctx.title); // 处理 {{ title }}
"Count: " + _toDisplayString(_ctx.count); // 处理 Count: {{ count }}
-
补丁标记:
1 /* TEXT */
:表示节点包含动态文本内容- 用于优化更新性能
-
块级树优化:
- 使用
_openBlock
和_createElementBlock
创建块级树 - 优化更新性能
- 跟踪动态节点
- 使用
3. 优化说明
-
静态内容优化:
- 静态的 class 属性被提升到渲染函数外部
- 减少每次渲染时的对象创建
-
事件处理优化:
- 使用事件处理器缓存
- 避免不必要的函数重建
-
动态内容标记:
- 使用补丁标记(patch flags)标识动态内容
- 优化更新性能
-
块级树优化:
- 创建块级树结构
- 精确跟踪动态节点
- 提高更新性能
这个生成的代码展示了 Vue 编译器的多项优化策略:
- 静态内容提升
- 事件处理器缓存
- 补丁标记
- 块级树优化
- 动态节点追踪
四、举例说明生成代码的详细过程
让我们按照 generate
函数的执行流程,详细分析代码生成的每个步骤:
4.1 创建上下文和初始设置
源码分析:
js//
const context = createCodegenContext(ast, options);
if (options.onContextCreated) options.onContextCreated(context);
// 2. 从上下文中解构需要的工具函数和状态
const {
mode, // 编译模式:module/function
push, // 用于向代码字符串追加内容
prefixIdentifiers, // 是否添加标识符前缀
indent, // 增加缩进级别
deindent, // 减少缩进级别
newline, // 插入换行
scopeId, // 作用域ID
ssr, // 是否服务端渲染
} = context;
// 3. 获取辅助函数信息
const helpers = Array.from(ast.helpers);
const hasHelpers = helpers.length > 0;
const useWithBlock = !prefixIdentifiers && mode !== "module";
context 对象变化:
js
// 初始状态
context = {
mode: "module",
code: "",
column: 1,
line: 1,
offset: 0,
indentLevel: 0,
pure: false,
map: undefined,
source: ast.source,
// ... 其他属性
};
// helpers 数组内容(基于我们的示例)
helpers = [
CREATE_ELEMENT_VNODE,
TO_DISPLAY_STRING,
OPEN_BLOCK,
CREATE_ELEMENT_BLOCK,
];
4.2 生成前导代码
源码分析:
js
// 1. 创建前导代码上下文
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context;
// 2. 根据模式生成不同的前导代码
if (!__BROWSER__ && mode === "module") {
genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined);
} else {
genFunctionPreamble(ast, preambleContext);
}
模块模式下的前导代码生成:
js
function genModulePreamble(ast, context, genScopeId, inline) {
const { push, newline, runtimeModuleName } = context;
// 1. 生成辅助函数导入
if (ast.helpers.size) {
push(
`import { ${helpers
.map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(", ")} } from ${JSON.stringify(runtimeModuleName)}\n`
);
}
// 2. 生成静态提升的变量
genHoists(ast.hoists, context);
newline();
// 3. 添加 export 关键字
if (!inline) {
push(`export `);
}
}
代码生成过程:
js
// context.code 的变化
context.code = ''
↓
// 添加导入语句
context.code = `import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from "vue"\n`
↓
// 添加静态提升的内容
context.code += `\nconst _hoisted_1 = { class: "container" }\n`
↓
// 添加 export 关键字
context.code += `\nexport `
4.3 生成渲染函数签名
源码分析:
js
// 1. 确定函数名和参数
const functionName = ssr ? `ssrRender` : `render`;
const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];
// 2. 生成函数声明
if (isSetupInlined) {
push(`(${signature}) => {`);
} else {
push(`function ${functionName}(${args.join(", ")}) {`);
}
indent();
// 3. 处理 with 块(如果需要)
if (useWithBlock) {
push(`with (_ctx) {`);
indent();
// 在 with 块内声明辅助函数
if (hasHelpers) {
push(
`const { ${helpers.map(aliasHelper).join(", ")} } = _Vue\n`,
NewlineType.End
);
newline();
}
}
代码生成过程:
js
// context.code 的变化
context.code += `function render(_ctx, _cache) {`
↓
context.code += `\n ` // 缩进增加
4.4 生成 VNode 树
源码分析:
js
// 1. 生成返回语句
if (!ssr) {
push(`return `);
}
// 2. 生成根节点
if (ast.codegenNode) {
genNode(ast.codegenNode, context);
} else {
push(`null`);
}
节点生成的核心函数:
js
// genNode 是代码生成的核心函数,负责处理所有类型的节点
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
// 1. 处理字符串节点
if (isString(node)) {
context.push(node, NewlineType.Unknown);
return;
}
// 2. 处理符号节点(通常是辅助函数)
if (isSymbol(node)) {
context.push(context.helper(node));
return;
}
// 3. 根据节点类型分发到不同的处理函数
switch (node.type) {
// 3.1 元素、条件和循环节点
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
// 确保节点已经过转换
genNode(node.codegenNode!, context);
break;
// 3.2 文本节点
case NodeTypes.TEXT:
genText(node, context);
break;
// 3.3 简单表达式
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context);
break;
// 3.4 插值表达式 {{ xxx }}
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
// 3.5 虚拟节点调用
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context);
break;
// 3.6 复合表达式
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context);
break;
// ... 其他类型的处理
}
}
// 处理文本节点
function genText(node: TextNode, context: CodegenContext) {
context.push(JSON.stringify(node.content), NewlineType.Unknown, node);
}
// 处理表达式节点
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node;
context.push(
isStatic ? JSON.stringify(content) : content,
NewlineType.Unknown,
node
);
}
// 处理插值表达式
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
const { push, helper, pure } = context;
if (pure) push(PURE_ANNOTATION);
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(`)`);
}
// 处理虚拟节点调用
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure } = context;
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent,
} = node;
// 1. 处理指令
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`);
}
// 2. 处理块级节点
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `);
}
// 3. 添加纯注释标记
if (pure) {
push(PURE_ANNOTATION);
}
// 4. 选择创建节点的辅助函数
const callHelper = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent);
push(helper(callHelper) + `(`, NewlineType.None, node);
// 5. 生成参数列表
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
);
push(`)`);
// 6. 处理块级和指令的结束
if (isBlock) {
push(`)`);
}
if (directives) {
push(`, `);
genNode(directives, context);
push(`)`);
}
}
// 处理节点列表生成
function genNodeList(
nodes: (string | symbol | CodegenNode)[],
context: CodegenContext
) {
const { push, newline } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(node);
} else if (isSymbol(node)) {
push(context.helper(node));
} else {
genNode(node, context);
}
if (i < nodes.length - 1) {
push(", ");
}
}
}
// 处理可能为空的参数列表
function genNullableArgs(args: any[]): any[] {
// 从后往前寻找第一个非空参数
let i = args.length;
while (i--) {
if (args[i] != null) break;
}
// 返回截至最后一个非空参数的列表
return args.slice(0, i + 1);
}
代码生成过程:
js
// 1. 初始状态
context = {
code: `function render(_ctx, _cache) {\n return `,
indentLevel: 1,
line: 2,
column: 9,
};
// 2. 生成根节点开始
context.code += `(_openBlock(), _createElementBlock("div", _hoisted_1, [`;
context.column += 52;
// 3. 生成 h1 节点
context.code += `\n _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),`;
context.line += 1;
context.column = 78;
// 4. 生成 button 节点
context.code += `\n _createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "Count: " + _toDisplayString(_ctx.count), 1 /* TEXT */)`;
context.line += 3;
context.column = 63;
// 5. 完成节点生成
context.code += `\n]))`;
context.line += 1;
context.column = 4;
在这个过程中,genNode
函数通过递归遍历 AST 节点树,为每种类型的节点生成相应的代码:
- 元素节点 :生成
createElementVNode
或createElementBlock
调用 - 文本节点:直接生成字符串字面量
- 插值表达式 :生成
toDisplayString
调用 - 指令:生成相应的指令处理代码
- 事件处理器:生成带缓存的事件处理函数
每个节点的生成都会考虑:
- 静态/动态内容的处理
- 优化标记的添加
- 缓存策略的应用
- 块级树的构建
4.5 生成函数结束
源码分析:
js
// 1. 处理 with 块结束
if (useWithBlock) {
deindent();
push(`}`);
}
// 2. 结束渲染函数
deindent();
push(`}`);
// 3. 返回生成结果
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
map: context.map ? context.map.toJSON() : undefined,
};
最终生成的完整代码:
js
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";
const _hoisted_1 = { class: "container" };
export function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock("div", _hoisted_1, [
_createElementVNode(
"h1",
null,
_toDisplayString(_ctx.title),
1 /* TEXT */
),
_createElementVNode(
"button",
{
onClick:
_cache[0] ||
(_cache[0] = (...args) =>
_ctx.handleClick && _ctx.handleClick(...args)),
},
"Count: " + _toDisplayString(_ctx.count),
1 /* TEXT */
),
])
);
}
下面通过一个简单的流程图将整个代码生成过程过一遍。
js
模板 Template
↓
解析 Parse
↓
AST 生成
↓
转换 Transform
↓
代码生成 CodeGen
|
├── 1. 创建上下文
| ├── 初始化 context 对象
| └── 收集 helpers
|
├── 2. 生成前导代码
| ├── 导入辅助函数
| ├── 静态提升
| └── 导出声明
|
├── 3. 生成渲染函数
| ├── 函数签名
| └── with 块处理
|
├── 4. 生成 VNode 树
| ├── genNode
| | ├── 文本节点
| | ├── 表达式节点
| | ├── 插值节点
| | └── VNode 调用
| |
| └── 优化处理
| ├── 静态提升
| ├── 补丁标记
| ├── 块级树
| └── 事件缓存
|
└── 5. 完成生成
├── 闭合函数
└── 返回结果
五、优化说明
让我们基于前面的示例模板来分析各项优化策略:
vue
<template>
<div class="container">
<h1>{{ title }}</h1>
<button @click="handleClick">Count: {{ count }}</button>
</div>
</template>
1. 静态提升 (Static Hoisting)
在我们的示例中,静态的 class 属性被提升:
js
// 静态提升的内容
const _hoisted_1 = { class: "container" };
// 渲染函数中直接使用提升的内容
return _createElementBlock("div", _hoisted_1, [
// ... 子节点
]);
这种优化的效果:
- 将静态的 class="container" 提升到渲染函数外部
- 避免在每次渲染时重新创建对象
- 减少内存分配和垃圾回收的开销
2. 动态内容处理 (Dynamic Content)
示例中有两处动态内容:
js
// 1. h1 中的动态文本
_createElementVNode(
"h1",
null,
_toDisplayString(_ctx.title), // 动态内容
1 /* TEXT */ // 补丁标记
);
// 2. button 中的动态文本
_createElementVNode(
"button",
{ onClick: /*...*/ },
"Count: " + _toDisplayString(_ctx.count), // 动态内容
1 /* TEXT */ // 补丁标记
);
优化效果:
- 使用
_toDisplayString
处理动态文本 - 通过补丁标记
1 /* TEXT */
标识动态内容 - 更新时只处理文本部分,不影响节点本身
3. 事件处理优化 (Event Handling)
示例中的点击事件处理:
js
_createElementVNode(
"button",
{
onClick:
_cache[0] ||
(_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args)),
}
// ... 内容
);
优化效果:
- 使用
_cache[0]
缓存事件处理函数 - 避免每次渲染时重新创建函数
- 处理方法可能不存在的情况 (
_ctx.handleClick && _ctx.handleClick
)
4. 块级树优化 (Block Tree)
示例中的块级树结构:
js
// 1. 开启块级作用域
_openBlock();
// 2. 创建块级元素
_createElementBlock("div", _hoisted_1, [
// 子节点被收集到 block 的 dynamicChildren 中
_createElementVNode("h1", null, _toDisplayString(_ctx.title), 1),
_createElementVNode("button", /*...*/, /*...*/, 1)
]);
优化效果:
- 根节点
div
被标记为块级节点 - 动态子节点 (h1 和 button) 被收集到 dynamicChildren 数组
- 更新时可以直接定位到动态节点,跳过静态内容
5. 优化策略的综合应用
让我们看看这些优化策略如何在示例中协同工作:
js
import /*...*/ "vue";
// 1. 静态提升
const _hoisted_1 = { class: "container" };
// 2. 渲染函数
export function render(_ctx, _cache) {
return (
_openBlock(),
_createElementBlock("div", _hoisted_1, [
// 动态文本节点
_createElementVNode(
"h1",
null,
_toDisplayString(_ctx.title),
1 /* TEXT */
),
// 带缓存的事件处理器和动态文本
_createElementVNode(
"button",
{
onClick:
_cache[0] ||
(_cache[0] = (...args) =>
_ctx.handleClick && _ctx.handleClick(...args)),
},
"Count: " + _toDisplayString(_ctx.count),
1 /* TEXT */
),
])
);
}
这个示例展示了优化策略的综合效果:
- 静态的 class 属性被提升
- 动态文本内容使用补丁标记
- 事件处理器被缓存
- 使用块级树结构追踪动态节点
- 生成的代码既简洁又高效
六、总结
代码生成是vue编译过程的最后一步,它将优化后的 AST 转换为可执行的渲染函数,在这个过程中,编译器实现了多种优化策略,如静态提升、动态内容标记、事件缓存、块树优化。至此,我们完成了整个编译流程的分析。