10.模板编译
约 1205 字大约 4 分钟
2026-03-01
面试题
- 说一下 Vue 中 Compiler 的实现原理是什么?
- 说一下 Vue3 在进行模板编译时做了哪些优化?
概述
Vue 的编译器将模板转换为渲染函数:
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>↓ 编译为 ↓
function render() {
return h('div', [h('h1', { id: someId }, 'Hello')])
}
| 组件 | 职责 |
|---|---|
| 解析器 | 模板 → 模板 AST |
| 转换器 | 模板 AST → JavaScript AST |
| 生成器 | JavaScript AST → 渲染函数 |
一、解析器
1. 有限状态机 (FSM)
解析器用有限状态机逐字符解析模板。以 <p>Vue</p> 为例:
- 初始状态 → 读到
<→ 标签开始 - 读到
p→ 标签名称 - 读到
>→ 记录标签 p,回到初始 - 读到
V、u、e→ 文本状态 - 读到
<→ 标签开始,/→ 标签结束,p→ 结束标签名,>→ 回到初始

2. Tokenize
const template = '<p>Vue</p>'
const State = {
initial: 1, tagOpen: 2, tagName: 3,
text: 4, tagEnd: 5, tagEndName: 6
}
// tokenize(str) 输出:
[
{ type: 'tag', name: 'p' },
{ type: 'text', content: 'Vue' },
{ type: 'tagEnd', name: 'p' }
]3. 构造模板 AST
扫描 token 列表,用栈维护父子关系:遇到开始标签压栈,遇到结束标签出栈。
以 <div><p>Vue</p><p>React</p></div> 为例,得到的 AST 结构:
Root
└── Element(div)
├── Element(p) → Text(Vue)
└── Element(p) → Text(React)
function parse(str) {
const tokens = tokenize(str)
const root = { type: 'Root', children: [] }
const elementStack = [root]
while (tokens.length) {
const parent = elementStack[elementStack.length - 1]
const t = tokens[0]
switch (t.type) {
case 'tag':
const elementNode = { type: 'Element', tag: t.name, children: [] }
parent.children.push(elementNode)
elementStack.push(elementNode)
break
case 'text':
parent.children.push({ type: 'Text', content: t.content })
break
case 'tagEnd':
elementStack.pop()
break
}
tokens.shift()
}
return root
}二、转换器
将模板 AST 转为 JavaScript AST,分为「遍历与转换」和「生成 JS AST」。
1. 遍历与转换
维护上下文:currentNode、childIndex、parent、nodeTransforms。转换函数可返回 exit 回调,在子节点处理完后再执行(类似 React 的 completeWork、Koa 洋葱模型)。
const context = {
currentNode: null, childIndex: 0, parent: null,
replaceNode(node) { /* ... */ },
removeNode() { /* ... */ },
nodeTransforms: [transformRoot, transformElement, transformText]
}
function tranverseNode(ast, context) {
context.currentNode = ast
const exitFns = []
for (const transform of context.nodeTransforms) {
const onExit = transform(context.currentNode, context)
if (onExit) exitFns.push(onExit)
if (!context.currentNode) return
}
// 递归子节点...
let i = exitFns.length
while (i--) exitFns[i]() // 子节点处理完后执行
}2. 生成 JS AST
<div><p>Vue</p><p>React</p></div> → function render(){ return h('div', [h('p','Vue'), h('p','React')]) }
对应 JS AST 结构:FunctionDecl → ReturnStatement → CallExpression(h) → StringLiteral + ArrayExpression → CallExpression...
function transformText(node) {
if (node.type !== 'Text') return
node.jsNode = createStringLiteral(node.content)
}
function transformElement(node, context) {
return () => {
if (node.type !== 'Element') return
const callExp = createCallExpression('h', [createStringLiteral(node.tag)])
node.children.length === 1
? callExp.arguments.push(node.children[0].jsNode)
: callExp.arguments.push(createArrayExpression(node.children.map(c => c.jsNode)))
node.jsNode = callExp
}
}
function transformRoot(node) {
return () => {
if (node.type !== 'Root') return
node.jsNode = {
type: 'FunctionDecl',
id: { type: 'Identifier', name: 'render' },
params: [],
body: [{ type: 'ReturnStatement', return: node.children[0].jsNode }]
}
}
}三、生成器
根据 JS AST 拼接字符串,得到渲染函数代码:
function generate(ast) {
const context = {
code: '', currentIndent: 0,
push(code) { context.code += code },
newLine() { context.code += '\n' + ' '.repeat(context.currentIndent) },
indent() { context.currentIndent++; context.newLine() },
deIndent() { context.currentIndent--; context.newLine() }
}
genNode(ast, context)
return context.code
}
// genNode 按节点类型分发:genFunctionDecl、genReturnStatement、genCallExpression、genStringLiteral、genArrayExpression四、Vue3 模板编译优化
1. 静态提升
将静态节点提升到渲染函数外,避免每次渲染重复创建:
const _hoisted_1 = createStaticVNode("<p>这是一个静态的段落。</p>", 1)
export function render(_ctx, _cache) {
return createElementBlock("div", null, [
_hoisted_1,
createElementVNode("p", null, toDisplayString(_ctx.dynamicMessage), 1 /* TEXT */)
])
}静态属性也可提升:const _hoisted_1 = { class: "btn btn-primary" }
2. 预字符串化
大量连续静态内容(约 10 个节点以上)会编译为字符串节点,减少 VNode 数量,加快 diff,SSR 时减少计算。
const _hoisted_2 = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul>...</ul>", 2)

3. 缓存内联事件处理函数
// Vue2: 每次渲染创建新函数
onClick: function($event) { ctx.count++ }
// Vue3: 缓存
onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))4. Block Tree
Block 节点有 dynamicChildren 数组,仅存储动态子节点。更新时只对比 dynamicChildren,跳过静态节点。

根节点、带 v-if/v-else/v-for 的节点会作为 Block(因结构可能变化)。
5. PatchFlag
标记动态部分类型,更新时只检查标记处:
- TEXT、CLASS、STYLE、PROPS、FULL_PROPS
- HYDRATE_EVENTS、STABLE_FRAGMENT、KEYED_FRAGMENT、UNKEYED_FRAGMENT
createElementBlock("div", { class: _normalizeClass($setup.user), "data-id": "1", title: "user name" },
_toDisplayString($setup.user.name), 3 /* TEXT | CLASS */)面试题参考答案
Vue 中 Compiler 的实现原理?Vue3 模板编译做了哪些优化?
Compiler 原理:解析器(FSM + tokenize + 栈构造 AST)→ 转换器(上下文 + 进入/退出回调,生成 JS AST)→ 生成器(遍历 JS AST 拼接字符串)。
Vue3 优化:
- 静态提升:静态节点/属性提到函数外
- 预字符串化:连续静态内容编译为字符串,减少 VNode
- 缓存内联事件:避免每次渲染创建新函数
- Block Tree:dynamicChildren 跳过静态节点比较
- PatchFlag:只对比动态部分
-EOF-
