Vue模板编译原理
发表于更新于
vueVue模板编译原理
OHNIIVue模板编译原理
核心流程(记忆关键)
记忆口诀:解析成树,优化标记,生成函数
三大阶段: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>
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
| 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> `
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', 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
| { 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)' }] } ] }
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
| <p v-if="show">{{ message }}</p>
(show) ? _c('p', [_v(_s(message))]) : _e()
<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))]) })
<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
| 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))]) ]) } }
|
Vue 3 的编译优化
1. 静态提升(Static Hoisting)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const _hoisted_1 = { id: 'app' } const _hoisted_2 = _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) ])) }
|
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
| export const enum PatchFlags { TEXT = 1, CLASS = 1 << 1, STYLE = 1 << 2, PROPS = 1 << 3, FULL_PROPS = 1 << 4, HYDRATE_EVENTS = 1 << 5, STABLE_FRAGMENT = 1 << 6, KEYED_FRAGMENT = 1 << 7, UNKEYED_FRAGMENT = 1 << 8, NEED_PATCH = 1 << 9, DYNAMIC_SLOTS = 1 << 10, HOISTED = -1, BAIL = -2 }
<div> <p>{{ message }}</p> <span :class="className">文本</span> </div>
_createElementVNode("p", null, _toDisplayString(message), 1 ) _createElementVNode("span", { class: className }, "文本", 2 )
|
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
| <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) ])) }
|
4. 事件缓存(Cache Handlers)
1 2 3 4 5 6 7 8 9 10 11 12 13
| <button @click="handleClick">点击</button>
<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
| <div> <p>静态文本1</p> <p>静态文本2</p> <p>静态文本3</p> </div>
const _ssrStatic = "<div><p>静态文本1</p><p>静态文本2</p><p>静态文本3</p></div>"
function ssrRender(_ctx, _push) { _push(_ssrStatic) }
|
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
| export default { render(h) { return h('div', { attrs: { id: 'app' } }, [ h('h1', this.title), h('p', this.message) ]) } }
|
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>
_l((list), function(item) { return (item.show) ? _c('div', [_v(_s(item.name))]) : _e() })
computed: { filteredList() { return this.list.filter(item => item.show) } } <div v-for="item in filteredList">{{ item.name }}</div>
<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
|
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
| <div v-once> <h1>{{ title }}</h1> <p>这些内容只渲染一次</p> </div>
<div v-memo="[value1, value2]"> </div>
<div v-for="item in list" :key="item.id"> </div>
|
技巧3:理解编译指令
1 2 3 4 5 6 7 8 9 10 11 12
| <div v-pre> {{ 这里不会被编译 }} </div>
<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. 总结
核心要点:
- 模板编译是将 template 转换为 render 函数的过程
- 三大阶段:Parse(解析)→ Optimize(优化)→ Generate(生成)
- 优化阶段标记静态节点,为 Diff 算法提供优化提示
- Vue 3 的编译优化(静态提升、Patch Flags、Block Tree)极大提升性能
- 推荐使用构建时编译(vue-loader),体积更小,性能更好
记忆口诀:
- 解析成树,优化标记,生成函数
- 静态提升,补丁标记,区块树优化
- 构建时编译,性能更优,体积更小