一、痛点引入:为什么你需要了解编译原理?
作为初中级工程师,你可能每天都在写 .vue 文件,但有没有想过:
- 浏览器根本不认识
.vue文件,它是怎么跑起来的? - 为什么
v-for和v-if不能同时用?编译阶段到底发生了什么? - 都说 Vue 3 比 Vue 2 快,"快"在哪里?是运行时优化还是编译时优化?
了解编译原理,不是为了让你去重写编译器,而是让你写出更高效的模板代码,在面试中能聊出深度,在日常开发中能避开性能坑。
二、编译流程概览:从 .vue 到浏览器可执行的 JS
一个 .vue 文件要经历这样的旅程才能被浏览器执行:

Vue 3 的编译器采用模块化设计,主要由三个包协同工作:
| 包名 | 职责 |
|---|---|
| @vue/compiler-sfc | 专门处理 .vue 文件,将文件拆分成 <template>、<script> 和 <style> 三大块 |
| @vue/compiler-dom | 专注于浏览器 DOM 平台相关的编译逻辑,是浏览器端的适配层 |
| @vue/compiler-core | 平台无关的核心编译器,实现了编译的三大核心阶段 |
💡 理解关键 :
compiler-core是心脏,compiler-dom是手脚,compiler-sfc是门面。
三、核心三部曲:Parse → Transform → Generate
Vue 模板编译的核心流程,就是 @vue/compiler-core 中发生的 Parse → Transform → Generate 三个阶段。
阶段一:Parse(解析)------ 从模板字符串到 AST
输入 :模板字符串(如 <div>{{ msg }}</div>)
输出:抽象语法树(AST)
AST 以树形结构描述模板的节点类型、属性、指令等信息。简单来说,就是把 HTML 字符串变成一棵 JS 对象树。
解析器的工作原理(伪代码)
解析器会从头到尾扫描模板字符串,根据字符类型决定如何解析:
javascript
while (模板字符串还有内容) {
if (以 '{{' 开头) {
// 解析插值表达式 {{ msg }}
node = parseInterpolation()
} else if (以 '<' 开头) {
if (以 '<!--' 开头) {
// 解析注释
node = parseComment()
} else if (以 '</' 开头) {
// 解析结束标签
parseTag(TagType.End)
} else if (是字母) {
// 解析开始标签
node = parseElement()
}
} else {
// 普通文本节点
node = parseText()
}
// 每解析完一个节点,就把已解析的字符串截断
advanceBy(已解析的长度)
}
实际例子
对于模板 <div name="test"><p>{{ msg }}</p></div>,解析过程是这样的:
less
原始字符串: <div name="test"><p>{{ msg }}</p></div>
第1步: 解析 <div,截断 → name="test"><p>{{ msg }}</p></div>
第2步: 解析 name="test",截断 → <p>{{ msg }}</p></div>
第3步: 解析 <p>,截断 → {{ msg }}</p></div>
第4步: 解析 {{ msg }},截断 → </p></div>
第5步: 解析 </p>,截断 → </div>
第6步: 解析 </div>,截断 → (空)
最终生成的 AST 节点类型包括:
ROOT:根节点ELEMENT:元素节点(如<div>)TEXT:文本节点INTERPOLATION:插值节点(如{{ msg }})DIRECTIVE:指令节点(如v-for、v-if)
💡 你可以在这里直观看到 AST :访问 astexplorer.net,选择
vue3-compiler,粘贴你的模板代码,就能看到生成的 AST 结构。
阶段二:Transform(转换)------ 为运行时优化做准备
输入 :Parse 阶段生成的模板 AST
输出:经过转换和优化的 JavaScript AST
Transform 阶段会做三件重要的事:
1. 处理指令(v-for、v-if、v-model 等)
编译器会识别模板中的指令,并将其转换为对应的代码生成节点。
例如,v-for="item in list" 会被转换成 _renderList(list, ...) 的调用。
2. 静态提升(Static Hoisting)------ Vue 3 最重要的优化之一
编译器会识别出不依赖任何响应式数据 的纯静态节点(如没有绑定、没有插值、没有指令的 <div>),并将其提升到渲染函数之外。
javascript
// 编译前 (模板)
<template>
<div class="static">我不会变</div> <!-- 静态节点 -->
<div>{{ dynamic }}</div> <!-- 动态节点 -->
</template>
// 编译后 (简化)
const _hoisted_1 = { class: "static" } // 👈 静态节点被提升到外面,只创建一次!
function render() {
return [
_createVNode("div", _hoisted_1, "我不会变"), // 直接复用
_createVNode("div", null, dynamic) // 每次重新创建
]
}
这样做的好处是:
- 避免每次渲染重新创建相同的虚拟 DOM
- 减少内存占用和 GC 压力
- Vue 3.5 进一步提升了静态节点树的复用率,减少虚拟 DOM 创建开销约 15-20%
3. 标记 PatchFlag(补丁标志)
转换器会为每个动态节点 标记一个 PatchFlag,指示该节点在更新时哪些部分可能发生变化。
javascript
// PatchFlag 示例
_createElementBlock("p", null, _toDisplayString($setup.msg), 1 /* TEXT */)
// 👆 这个 1 表示"文本内容可能变化"
运行时看到这个标志,只对比文本内容,跳过其他属性的对比,极大提升 diff 效率。
阶段三:Generate(生成)------ 从 AST 到 render 函数
输入 :Transform 阶段生成的 JavaScript AST
输出:可执行的 render 函数字符串
Generate 阶段的核心逻辑很简单:递归遍历 JavaScript AST,进行字符串拼接。
实际生成的 render 函数
对于模板 <p>{{ msg }}</p>,最终生成的 render 函数长这样:
javascript
import { toDisplayString as _toDisplayString, openBlock as _openBlock,
createElementBlock as _createElementBlock } from "vue";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(),
_createElementBlock("p", null, _toDisplayString($setup.msg), 1 /* TEXT */);
}
💡 注意 :
openBlock和createElementBlock是 Vue 3 的 Block Tree 优化 核心------openBlock初始化一个数组收集动态节点,createElementBlock生成根节点并将动态节点挂载到dynamicChildren属性上,让 diff 时只对比动态节点。
四、Vue 3.5 编译相关新特性
1. 响应式 Props 解构正式稳定
在 3.5 中,defineProps 搭配解构使用后,编译时 会自动将解构变量转换成 props.xxx:
vue
<!-- 源码 -->
<script setup>
const { name } = defineProps({ name: String })
console.log(name)
</script>
<!-- 编译后(简化) -->
<script>
setup(__props) {
console.log(__props.name) // 👈 自动变成了 props.name
}
</script>
这意味着你不再需要 toRefs(props),直接解构即可保持响应式。
2. 响应式系统底层重构(内存减少 56%)
Vue 3.5 重写了响应式系统的依赖存储结构,采用版本计数 + 双向链表,灵感来源于 Preact Signals。
虽然这主要是运行时优化,但编译阶段配合生成的代码更高效------更少的依赖意味着更少的更新触发。
五、完整数据流图:从模板到 DOM 更新

六、避坑指南
1. v-if 和 v-for 不要同时用
在同一个元素上同时使用 v-if 和 v-for 时,v-if 的优先级更高。这会导致:
vue
<!-- ❌ 错误:v-if 先执行,此时 item 还未定义 -->
<div v-for="item in list" v-if="item.active">{{ item.name }}</div>
<!-- ✅ 正确:先用 template 包裹 v-for,内部再 v-if -->
<template v-for="item in list">
<div v-if="item.active">{{ item.name }}</div>
</template>
2. 静态内容不要乱加响应式绑定
不必要的响应式绑定会阻止静态提升:
vue
<!-- ❌ 静态内容加了动态绑定,无法提升 -->
<div :class="'static-class'">文本</div>
<!-- ✅ 直接用静态属性,可以被提升 -->
<div class="static-class">文本</div>
3. 理解编译时 vs 运行时
| 概念 | 编译时(Compile Time) | 运行时(Runtime) |
|---|---|---|
| 做什么 | 模板 → render 函数 | 执行 render → VNode → DOM |
| 谁负责 | @vue/compiler-* 包 |
vue 运行时包 |
| 能优化的 | 静态提升、PatchFlag | diff 算法、响应式更新 |
| 开发者能干预的 | 模板写法(如用 class 代替 :class) | 组件结构、数据组织 |
七、总结
| 知识点 | 核心结论 |
|---|---|
| 编译三部曲 | Parse(模板→AST)→ Transform(AST→优化后AST)→ Generate(AST→render函数) |
| 静态提升 | 不变的节点提到渲染函数外,只创建一次,减少内存和 CPU 开销 |
| PatchFlag | 标记动态节点的变化类型,运行时只对比变化部分 |
| Block Tree | openBlock + createElementBlock 收集动态节点,实现靶向更新 |
| Vue 3.5 编译亮点 | Props 解构编译时转换、响应式系统内存减少 56% |
🚀 进阶学习路径:
- 去 astexplorer.net 选
vue3-compiler,亲手看看你的模板编译成什么样- 阅读欧阳的《Vue3 编译原理揭秘》开源电子书
- 在
node_modules/@vue/compiler-core中找到parse.ts、transform.ts、generate.ts,跟着 debug 走一遍
理解了编译原理,你写的每一行模板代码,在编译阶段会发生什么、能怎么优化,心里都有数了。这不仅是面试加分项,更是写出高性能 Vue 应用的底气。🎯