模板编译原理
问题
Vue 的模板是如何编译的?编译过程有哪些阶段?Vue 3 做了哪些编译优化?
答案
Vue 的模板编译是将模板字符串转换为渲染函数的过程。编译发生在构建时(SFC + 构建工具)或运行时(完整版 Vue)。
编译流程概览
三大阶段
| 阶段 | 输入 | 输出 | 作用 |
|---|---|---|---|
| Parse | 模板字符串 | AST | 词法分析 + 语法分析 |
| Transform | AST | 优化后的 AST | 静态分析 + 优化 |
| Generate | AST | render 函数 | 代码生成 |
1. Parse 阶段 - 解析
将模板字符串解析为抽象语法树(AST):
<!-- 模板 -->
<div id="app">
<span>{{ message }}</span>
</div>
// 解析后的 AST(简化)
const ast = {
type: 'Root',
children: [{
type: 'Element',
tag: 'div',
props: [{
type: 'Attribute',
name: 'id',
value: 'app'
}],
children: [{
type: 'Element',
tag: 'span',
children: [{
type: 'Interpolation',
content: {
type: 'Expression',
content: 'message'
}
}]
}]
}]
};
解析过程
// 简化的解析器
function parse(template: string): AST {
const context = {
source: template,
advance(num: number) {
this.source = this.source.slice(num);
}
};
const root: AST = {
type: 'Root',
children: parseChildren(context)
};
return root;
}
function parseChildren(context: Context): ASTNode[] {
const nodes: ASTNode[] = [];
while (!isEnd(context)) {
let node: ASTNode;
if (context.source.startsWith('{{')) {
// 解析插值 {{ }}
node = parseInterpolation(context);
} else if (context.source.startsWith('<')) {
// 解析元素 <div>
node = parseElement(context);
} else {
// 解析文本
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
2. Transform 阶段 - 转换与优化
对 AST 进行静态分析和优化:
function transform(ast: AST) {
const context = {
currentNode: null,
nodeTransforms: [
transformElement,
transformText,
transformInterpolation
]
};
traverseNode(ast, context);
// 添加 codegenNode(用于生成代码)
// 标记静态节点
// 提升静态内容
}
Vue 3 编译优化
PatchFlags(补丁标记)
<template>
<div>
<span>静态文本</span>
<span>{{ dynamic }}</span>
<span :class="cls">动态 class</span>
<span :id="id" :class="cls">多个动态属性</span>
</div>
</template>
编译后:
import { createVNode as _createVNode, toDisplayString as _toDisplayString } from 'vue';
// PatchFlags 枚举
const enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 2, // 动态 class
STYLE = 4, // 动态 style
PROPS = 8, // 动态 props(非 class/style)
FULL_PROPS = 16, // 有动态 key 的 props
// ...
}
function render() {
return _createVNode('div', null, [
// 静态节点:无 PatchFlag
_createVNode('span', null, '静态文本'),
// 动态文本:PatchFlag = 1 (TEXT)
_createVNode('span', null, _toDisplayString(dynamic), 1 /* TEXT */),
// 动态 class:PatchFlag = 2 (CLASS)
_createVNode('span', { class: cls }, '动态 class', 2 /* CLASS */),
// 多个动态属性:PatchFlag = 8 (PROPS) + 动态 key 数组
_createVNode('span', { id: id, class: cls }, '多个动态属性', 8 /* PROPS */, ['id', 'class'])
]);
}
Diff 时只检查有 PatchFlag 的节点,大幅提升性能。
静态提升(hoistStatic)
<template>
<div>
<span class="static">静态内容</span>
<span>{{ dynamic }}</span>
</div>
</template>
编译后:
// 静态节点被提升到渲染函数外部
const _hoisted_1 = _createVNode('span', { class: 'static' }, '静态内容');
function render() {
return _createVNode('div', null, [
_hoisted_1, // 复用,不重新创建
_createVNode('span', null, _toDisplayString(dynamic), 1 /* TEXT */)
]);
}
缓存事件处理函数
<template>
<button @click="handleClick">Click</button>
</template>
编译后:
function render(_ctx, _cache) {
return _createVNode('button', {
// 事件处理函数被缓存
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, 'Click');
}
Block Tree
<template>
<div><!-- Block -->
<span>静态</span>
<span>{{ dynamic1 }}</span>
<div v-if="show"><!-- Block -->
<span>{{ dynamic2 }}</span>
</div>
</div>
</template>
编译后,动态节点被收集到 Block 中:
function render() {
return (_openBlock(), _createBlock('div', null, [
_createVNode('span', null, '静态'),
_createVNode('span', null, _toDisplayString(dynamic1), 1 /* TEXT */),
show
? (_openBlock(), _createBlock('div', { key: 0 }, [
_createVNode('span', null, _toDisplayString(dynamic2), 1 /* TEXT */)
]))
: _createCommentVNode('v-if')
]));
// Block 的 dynamicChildren 只包含动态节点
}
Diff 时只需对比 dynamicChildren,跳过静态节点。
3. Generate 阶段 - 代码生成
将 AST 转换为渲染函数代码:
function generate(ast: AST): string {
const context = {
code: '',
push(code: string) {
this.code += code;
}
};
// 生成代码
context.push('function render() {');
context.push(' return ');
genNode(ast.children[0], context);
context.push('}');
return context.code;
}
function genNode(node: ASTNode, context: Context) {
switch (node.type) {
case 'Element':
genElement(node, context);
break;
case 'Text':
genText(node, context);
break;
case 'Interpolation':
genInterpolation(node, context);
break;
}
}
完整编译示例
<!-- 源模板 -->
<template>
<div id="app" class="container">
<h1>{{ title }}</h1>
<p v-if="show">Hello</p>
<button @click="handleClick">Click</button>
</div>
</template>
// 编译结果
import {
createVNode as _createVNode,
toDisplayString as _toDisplayString,
openBlock as _openBlock,
createBlock as _createBlock,
createCommentVNode as _createCommentVNode
} from 'vue';
// 静态提升
const _hoisted_1 = { id: 'app', class: 'container' };
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock('div', _hoisted_1, [
_createVNode('h1', null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_ctx.show
? (_openBlock(), _createBlock('p', { key: 0 }, 'Hello'))
: _createCommentVNode('v-if', true),
_createVNode('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
}, 'Click')
]));
}
常见面试问题
Q1: Vue 的编译发生在什么时候?
答案:
| 场景 | 编译时机 | 说明 |
|---|---|---|
| SFC + 构建工具 | 构建时 | vite/webpack 插件处理 |
| 运行时模板 | 运行时 | 需要完整版 Vue |
| JSX/TSX | 构建时 | Babel/esbuild 处理 |
// 构建时编译(推荐)
// .vue 文件在构建时被编译,产物只包含 render 函数
// 运行时编译(需要完整版 Vue)
import { createApp } from 'vue'; // 需要 'vue/dist/vue.esm-bundler.js'
createApp({
template: '<div>{{ msg }}</div>', // 运行时编译
data: () => ({ msg: 'Hello' })
});
Q2: Vue 3 编译优化有哪些?
答案:
| 优化 | 说明 | 效果 |
|---|---|---|
| PatchFlags | 标记动态内容类型 | Diff 时精确更新 |
| 静态提升 | 静态节点只创建一次 | 减少内存分配 |
| 事件缓存 | 缓存事件处理函数 | 避免子组件重渲染 |
| Block Tree | 收集动态节点 | Diff 跳过静态节点 |
| 预字符串化 | 连续静态节点合并为字符串 | 减少 VNode 数量 |
Q3: 为什么 Vue 要使用虚拟 DOM 而不是直接编译成命令式 DOM 操作?
答案:
// 直接编译成命令式操作
// 理论上更快,但有问题:
function render() {
const div = document.createElement('div');
div.textContent = this.msg; // 每次都重新设置
}
// 问题:
// 1. 难以处理条件渲染和列表渲染的复杂情况
// 2. 难以跨平台(SSR、小程序)
// 3. 更新时难以最小化 DOM 操作
虚拟 DOM 的优势:
- 声明式编程:描述结果,框架处理更新
- 跨平台:可渲染到不同目标
- 可预测:通过 Diff 确保最小化更新
- Vue 3 的平衡:编译时优化 + 运行时高效
Q4: 什么是 Block Tree?
答案:
Block Tree 是 Vue 3 的优化策略:
- Block:一个稳定结构的节点(无 v-if/v-for 的区域)
- dynamicChildren:Block 内的动态节点数组
- Diff 优化:只对比 dynamicChildren,跳过静态子节点
// 普通 VNode Tree:需要完整遍历
// Block Tree:只遍历动态节点
// 示例:100 个静态节点 + 1 个动态节点
// 普通 Diff:101 次比较
// Block Diff:1 次比较
Q5: 如何查看 Vue 组件编译后的代码?
答案:
- Vue SFC Playground:https://play.vuejs.org
- Vite 查看:开发模式下看 Network 面板
- 构建产物:查看 dist 目录
- Vue 编译器 API:
import { compile } from '@vue/compiler-dom';
const { code } = compile(`
<div>
<span>{{ msg }}</span>
</div>
`);
console.log(code);