Vue模板编译原理

Vue模板编译原理

核心流程(记忆关键)

记忆口诀:解析成树,优化标记,生成函数

三大阶段:Parse(解析)→ Optimize(优化)→ Generate(生成)


1. 基础概念

什么是模板编译?

  • 将 Vue 的 <template> 模板字符串转换为浏览器可执行的 JavaScript 代码(render 函数)
  • 模板编译发生在构建时(使用 vue-loader)或运行时(使用完整版 Vue)
  • 编译后的 render 函数返回虚拟 DOM(VNode)

为什么需要模板编译?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 开发者编写的模板
<template>
<div id="app">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
</div>
</template>

// 编译后的 render 函数(简化版)
function render() {
return _c('div', { attrs: { id: 'app' } }, [
_c('h1', [_v(_s(title))]),
_c('p', [_v(_s(message))])
])
}

浏览器无法直接识别 Vue 模板语法,必须转换为标准 JavaScript


2. 编译三大阶段详解

阶段1:Parse(解析)- 模板 → AST

目标:将 HTML 模板字符串解析成抽象语法树(AST)

核心原理:

1
2
3
4
5
// 使用正则表达式匹配 HTML 标签
const startTagOpen = /^<([a-zA-Z_][\w\-\.]*)/ // 开始标签
const startTagClose = /^\s*(\/?)>/ // 开始标签结束
const endTag = /^<\/([a-zA-Z_][\w\-\.]*)>/ // 结束标签
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 属性

解析过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 输入模板
const template = `
<div id="app" class="container">
<h1>{{ title }}</h1>
<p v-if="show">{{ message }}</p>
</div>
`

// 解析后的 AST(简化版)
const ast = {
type: 1, // 元素节点
tag: 'div',
attrsList: [
{ name: 'id', value: 'app' },
{ name: 'class', value: 'container' }
],
attrsMap: {
id: 'app',
class: 'container'
},
children: [
{
type: 1,
tag: 'h1',
children: [
{
type: 2, // 表达式节点
expression: '_s(title)',
text: '{{ title }}'
}
]
},
{
type: 1,
tag: 'p',
if: 'show', // v-if 指令
ifConditions: [{
exp: 'show',
block: { /* 当前节点 */ }
}],
children: [
{
type: 2,
expression: '_s(message)',
text: '{{ message }}'
}
]
}
]
}

AST 节点类型:

  • type: 1 - 元素节点(div、span 等)
  • type: 2 - 表达式节点()
  • type: 3 - 纯文本节点(hello world)

解析流程:

1
2
3
4
5
1. 从前往后遍历模板字符串
2. 匹配到开始标签 → 创建 AST 节点,入栈
3. 匹配到文本内容 → 创建文本节点,添加到栈顶节点的 children
4. 匹配到结束标签 → 栈顶节点出栈,添加到父节点的 children
5. 重复 2-4,直到模板字符串解析完毕

阶段2:Optimize(优化)- 标记静态节点

目标:遍历 AST,标记静态节点和静态根节点,为 Diff 算法优化做准备

什么是静态节点?

1
2
3
4
5
6
7
8
// 静态节点(永远不会变化)
<p>这是纯文本</p>
<div class="static">静态内容</div>

// 动态节点(可能会变化)
<p>{{ message }}</p>
<div :class="dynamicClass">动态内容</div>
<button @click="handleClick">点击</button>

静态节点的判断条件:

  • 纯文本节点
  • 没有动态绑定(v-bind、:、v-on、@)
  • 没有 v-if、v-for、v-else 等指令
  • 不是内置组件(slot、component)
  • 不是组件(必须是平台保留标签,如 div、span)
  • 父节点不是带 v-for 的 template
  • 所有子节点都是静态节点

优化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function optimize(root) {
if (!root) return

// 第一遍:标记所有静态节点
markStatic(root)

// 第二遍:标记静态根节点
markStaticRoots(root)
}

function markStatic(node) {
node.static = isStatic(node)

if (node.type === 1) { // 元素节点
// 遍历子节点
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
markStatic(child)

// 如果子节点不是静态的,父节点也不是静态的
if (!child.static) {
node.static = false
}
}
}
}

function markStaticRoots(node) {
if (node.type === 1) {
// 节点本身是静态节点 && 有子节点 && 子节点不只是一个纯文本节点
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}

// 递归标记子节点
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
markStaticRoots(node.children[i])
}
}
}
}

标记后的 AST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
type: 1,
tag: 'div',
static: false, // 因为有动态子节点
staticRoot: false,
children: [
{
type: 1,
tag: 'p',
static: true, // 纯文本,静态节点
staticRoot: true, // 静态根节点
children: [
{ type: 3, text: '这是纯文本', static: true }
]
},
{
type: 1,
tag: 'p',
static: false, // 有插值表达式,动态节点
children: [
{ type: 2, expression: '_s(message)' }
]
}
]
}

优化的意义:

  • 在 Diff 算法中,直接跳过静态节点的比对
  • 静态根节点会被提升(hoisted),只创建一次,后续直接复用
  • 极大提升更新性能,尤其是大型应用

阶段3:Generate(生成)- AST → render 函数

目标:将优化后的 AST 转换为可执行的 render 函数代码字符串

生成过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function generate(ast) {
const code = genElement(ast)
return {
render: `with(this){return ${code}}`
}
}

function genElement(el) {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else if (el.for && !el.forProcessed) {
return genFor(el)
} else {
// 普通元素
const data = genData(el)
const children = genChildren(el)
return `_c('${el.tag}'${data ? `,${data}` : ''}${children ? `,${children}` : ''})`
}
}

示例转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// AST
{
type: 1,
tag: 'div',
attrsList: [{ name: 'id', value: 'app' }],
children: [
{
type: 1,
tag: 'h1',
children: [{ type: 2, expression: '_s(title)' }]
},
{
type: 1,
tag: 'p',
if: 'show',
children: [{ type: 2, expression: '_s(message)' }]
}
]
}

// 生成的 render 函数代码
with(this){
return _c('div', { attrs: { "id": "app" } }, [
_c('h1', [_v(_s(title))]),
(show) ? _c('p', [_v(_s(message))]) : _e()
])
}

render 函数中的辅助方法:

  • _c:createElement,创建元素 VNode
  • _v:createTextVNode,创建文本 VNode
  • _s:toString,转换为字符串
  • _e:createEmptyVNode,创建空 VNode
  • _l:renderList,渲染列表(v-for)
  • _t:renderSlot,渲染插槽

指令的编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// v-if 编译
<p v-if="show">{{ message }}</p>
// 生成
(show) ? _c('p', [_v(_s(message))]) : _e()

// v-for 编译
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
// 生成
_l((list), function(item) {
return _c('li', { key: item.id }, [_v(_s(item.name))])
})

// v-model 编译(input)
<input v-model="message">
// 生成
_c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (message),
expression: "message"
}],
domProps: { "value": (message) },
on: {
"input": function($event) {
if($event.target.composing) return;
message = $event.target.value
}
}
})

// 事件编译
<button @click="handleClick">点击</button>
// 生成
_c('button', { on: { "click": handleClick } }, [_v("点击")])

3. Vue 2 vs Vue 3 编译优化对比

Vue 2 的编译特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Vue 2 编译结果
function render() {
with(this) {
return _c('div', { attrs: { id: 'app' } }, [
_c('h1', [_v(_s(title))]),
_c('p', { staticClass: 'text' }, [_v("静态文本")]),
_c('span', [_v(_s(message))])
])
}
}

// 问题:
// 1. 每次渲染都会重新创建所有 VNode(包括静态节点)
// 2. Diff 时需要遍历整棵树
// 3. 使用 with 语句,性能较差

Vue 3 的编译优化

1. 静态提升(Static Hoisting)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Vue 3 编译结果
const _hoisted_1 = { id: 'app' }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", { class: "text" }, "静态文本", -1)

function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createElementVNode("h1", null, _toDisplayString(_ctx.title), 1),
_hoisted_2, // 静态节点被提升,直接复用
_createElementVNode("span", null, _toDisplayString(_ctx.message), 1)
]))
}

// 优势:
// - 静态节点只创建一次,后续直接复用
// - 减少内存分配和 GC 压力

2. 补丁标记(Patch Flags)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Vue 3 的 PatchFlags
export const enum PatchFlags {
TEXT = 1, // 动态文本节点
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性(除了 class 和 style)
FULL_PROPS = 1 << 4, // 有动态 key 的属性
HYDRATE_EVENTS = 1 << 5, // 有事件监听器
STABLE_FRAGMENT = 1 << 6, // 子节点顺序不变的 fragment
KEYED_FRAGMENT = 1 << 7, // 有 key 的 fragment
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的 fragment
NEED_PATCH = 1 << 9, // 需要 patch
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
HOISTED = -1, // 静态节点
BAIL = -2 // 退出优化
}

// 编译示例
<div>
<p>{{ message }}</p>
<span :class="className">文本</span>
</div>

// 生成代码
_createElementVNode("p", null, _toDisplayString(message), 1 /* TEXT */)
_createElementVNode("span", { class: className }, "文本", 2 /* CLASS */)

// 优势:
// - Diff 时只需要比对有标记的动态内容
// - 跳过静态内容和不会变化的属性

3. Block Tree(区块树)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Vue 3 的 Block 概念
<div>
<p>静态文本</p>
<p>{{ message }}</p>
<span>{{ count }}</span>
</div>

// 编译后
function render(_ctx) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1, // 静态节点
_createElementVNode("p", null, _toDisplayString(_ctx.message), 1),
_createElementVNode("span", null, _toDisplayString(_ctx.count), 1)
]))
}

// Block 会收集所有动态子节点到 dynamicChildren 数组
// Diff 时只遍历 dynamicChildren,不遍历整棵树

// 优势:
// - 将树的遍历降级为数组的遍历
// - 时间复杂度从 O(n) 降到 O(m),m 是动态节点数量

4. 事件缓存(Cache Handlers)

1
2
3
4
5
6
7
8
9
10
11
12
13
// Vue 2
<button @click="handleClick">点击</button>
// 每次渲染都会创建新的事件处理函数

// Vue 3
<button @click="handleClick">点击</button>
// 编译后
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick(...args)))
}, "点击")

// 优势:
// - 事件处理函数被缓存,避免子组件不必要的更新

5. SSR 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Vue 3 对纯静态内容的 SSR 优化
<div>
<p>静态文本1</p>
<p>静态文本2</p>
<p>静态文本3</p>
</div>

// 编译后(SSR)
const _ssrStatic = "<div><p>静态文本1</p><p>静态文本2</p><p>静态文本3</p></div>"

function ssrRender(_ctx, _push) {
_push(_ssrStatic)
}

// 优势:
// - 静态内容直接输出字符串,不需要创建 VNode
// - SSR 性能大幅提升

4. 经典面试题解析

题目1:模板编译的三个阶段分别做了什么?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. Parse(解析):
- 使用正则表达式解析 HTML 模板字符串
- 生成 AST(抽象语法树)
- 识别标签、属性、指令、插值表达式等

2. Optimize(优化):
- 遍历 AST,标记静态节点和静态根节点
- 为 Diff 算法提供优化提示
- 静态节点在更新时会被跳过

3. Generate(生成):
- 将 AST 转换为 render 函数代码字符串
- 使用 new Function 创建可执行的 render 函数
- render 函数返回虚拟 DOM(VNode)

题目2:什么是静态节点?为什么要标记静态节点?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
// 静态节点:内容永远不会变化的节点
<p>这是纯文本</p>
<div class="static">静态内容</div>

// 动态节点:内容可能变化的节点
<p>{{ message }}</p>
<div :class="dynamicClass">动态内容</div>

// 标记静态节点的原因:
1. Diff 优化:更新时直接跳过静态节点的比对
2. 静态提升:静态节点只创建一次,后续直接复用
3. 性能提升:减少不必要的 VNode 创建和比对

题目3:Vue 3 的编译优化有哪些?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 静态提升(Static Hoisting)
- 静态节点提升到 render 函数外部
- 只创建一次,后续直接复用

2. 补丁标记(Patch Flags)
- 标记动态内容的类型(TEXT、CLASS、STYLE 等)
- Diff 时只比对有标记的内容

3. Block Tree(区块树)
- 收集所有动态子节点到 dynamicChildren 数组
- 将树的遍历降级为数组的遍历

4. 事件缓存(Cache Handlers)
- 缓存事件处理函数
- 避免子组件不必要的更新

5. SSR 优化
- 静态内容直接输出字符串
- 不需要创建 VNode

5. 面试口述版本

面试官:请解释 Vue 的模板编译过程

回答框架:

“Vue 的模板编译是将开发者编写的 template 模板转换为浏览器可执行的 render 函数的过程,主要分为三个阶段:

第一阶段是 Parse(解析)
Vue 使用正则表达式从前往后遍历模板字符串,识别开始标签、结束标签、属性、文本等,最终生成一棵抽象语法树(AST)。AST 是用 JavaScript 对象来描述 DOM 结构的树形数据结构。

第二阶段是 Optimize(优化)
这是编译阶段最关键的性能优化步骤。Vue 会遍历 AST,标记出所有的静态节点和静态根节点。静态节点是指内容永远不会变化的节点,比如纯文本。标记的目的是在后续的 Diff 算法中,可以直接跳过这些静态节点的比对,极大提升更新性能。

第三阶段是 Generate(生成)
Vue 将优化后的 AST 转换为 render 函数的代码字符串,最后通过 new Function 创建可执行的 render 函数。这个 render 函数在执行时会返回虚拟 DOM(VNode),用于后续的渲染和更新。

Vue 3 的编译优化
Vue 3 在编译阶段做了大量优化,包括静态提升、补丁标记(Patch Flags)、Block Tree 等。其中最重要的是 Patch Flags,它会在编译时标记每个动态节点的具体类型(比如动态文本、动态 class 等),这样在 Diff 时就可以做到靶向更新,只比对真正变化的内容,性能提升非常显著。”


6. 高频追问

Q1: 为什么 Vue 需要编译?直接写 render 函数不行吗?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 可以直接写 render 函数
export default {
render(h) {
return h('div', { attrs: { id: 'app' } }, [
h('h1', this.title),
h('p', this.message)
])
}
}

// 但是:
// 1. 开发体验差:写起来繁琐,可读性差
// 2. 无法享受编译优化:静态提升、Patch Flags 等优化都在编译时完成
// 3. 模板更直观:HTML 结构更清晰,更符合开发习惯

// 所以 Vue 提供了模板语法,通过编译转换为 render 函数

Q2: 运行时编译和构建时编译有什么区别?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
运行时编译(Runtime + Compiler):
- 使用完整版 Vue(vue.js)
- 在浏览器中实时编译模板
- 体积更大(多了编译器代码)
- 性能较差(每次都要编译)
- 适合原型开发、在线编辑器

构建时编译(Runtime Only):
- 使用 vue-loader 在构建时编译
- 生产环境只需要运行时代码
- 体积更小(减少 30%)
- 性能更好(编译已完成)
- 推荐用于生产环境

Q3: AST 和虚拟 DOM 有什么区别?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AST(抽象语法树):
- 编译阶段的产物
- 描述模板的语法结构
- 用于静态分析和优化
- 只在编译时存在

虚拟 DOM(VNode):
- 运行时的产物
- 描述真实 DOM 的结构
- 用于 Diff 算法和渲染
- 每次渲染都会创建

关系:
AST → (Generate) → render 函数 → (执行) → 虚拟 DOM

Q4: v-if 和 v-for 为什么不能一起使用?

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 不推荐
<div v-for="item in list" v-if="item.show">{{ item.name }}</div>

// 编译后(Vue 2)
_l((list), function(item) {
return (item.show) ? _c('div', [_v(_s(item.name))]) : _e()
})

// 问题:
// 1. v-for 优先级高于 v-if(Vue 2)
// 2. 每次都会遍历整个列表,即使某些项不显示
// 3. 性能浪费

// 推荐写法1:先过滤
computed: {
filteredList() {
return this.list.filter(item => item.show)
}
}
<div v-for="item in filteredList">{{ item.name }}</div>

// 推荐写法2:外层包裹
<template v-if="show">
<div v-for="item in list">{{ item.name }}</div>
</template>

7. 实战技巧

技巧1:查看编译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在线查看编译结果
// Vue 2: https://template-explorer.vuejs.org/
// Vue 3: https://vue-next-template-explorer.netlify.app/

// 代码中查看
import { compile } from 'vue'

const { code } = compile(`
<div id="app">
<h1>{{ title }}</h1>
</div>
`)
console.log(code)

技巧2:优化编译性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 使用 v-once 标记静态内容
<div v-once>
<h1>{{ title }}</h1>
<p>这些内容只渲染一次</p>
</div>

// 2. 使用 v-memo 缓存子树(Vue 3.2+)
<div v-memo="[value1, value2]">
<!-- 只有 value1 或 value2 变化时才重新渲染 -->
</div>

// 3. 合理使用 key
<div v-for="item in list" :key="item.id">
<!-- 使用唯一 key 帮助 Diff 算法 -->
</div>

技巧3:理解编译指令

1
2
3
4
5
6
7
8
9
10
11
12
// v-pre:跳过编译
<div v-pre>
{{ 这里不会被编译 }}
</div>

// v-cloak:防止闪烁
<div v-cloak>
{{ message }}
</div>
<style>
[v-cloak] { display: none; }
</style>

8. 记忆路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
模板编译
├── 目标:template → render 函数
├── 时机:构建时(vue-loader)或运行时(完整版)

├── 三大阶段
│ ├── Parse(解析)
│ │ ├── 正则匹配 HTML 标签
│ │ ├── 生成 AST
│ │ └── 识别指令、插值等
│ │
│ ├── Optimize(优化)
│ │ ├── 标记静态节点
│ │ ├── 标记静态根节点
│ │ └── 为 Diff 优化做准备
│ │
│ └── Generate(生成)
│ ├── AST → 代码字符串
│ ├── new Function 创建 render
│ └── 返回虚拟 DOM

├── Vue 3 优化
│ ├── 静态提升
│ ├── Patch Flags
│ ├── Block Tree
│ ├── 事件缓存
│ └── SSR 优化

└── 实战应用
├── 查看编译结果
├── 优化编译性能
└── 理解编译指令

9. 总结

核心要点:

  1. 模板编译是将 template 转换为 render 函数的过程
  2. 三大阶段:Parse(解析)→ Optimize(优化)→ Generate(生成)
  3. 优化阶段标记静态节点,为 Diff 算法提供优化提示
  4. Vue 3 的编译优化(静态提升、Patch Flags、Block Tree)极大提升性能
  5. 推荐使用构建时编译(vue-loader),体积更小,性能更好

记忆口诀:

  • 解析成树,优化标记,生成函数
  • 静态提升,补丁标记,区块树优化
  • 构建时编译,性能更优,体积更小