渲染机制
本教程共 40 篇 · 第 35 篇 · 更新于 2026-07-29 · 约 7 分钟阅读
35. 渲染机制
本节目标:搞懂 Vue 怎么把模板变成真实 DOM,以及它为什么能高效更新。
虚拟 DOM 是什么
虚拟 DOM(Virtual DOM)是用 JavaScript 对象描述真实 DOM 节点的概念。每个虚拟节点(vnode)长这样:
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}
一个 vnode 就是普通 JS 对象,包含了创建真实 DOM 需要的所有信息。虚拟 DOM 树就是这些 vnode 嵌套组成的树。
为什么要用虚拟 DOM?因为它让你用声明式的方式构建 UI——你只描述”页面应该长什么样”,至于怎么操作 DOM,交给渲染器去处理。
渲染器的两个核心操作:
- Mount:遍历虚拟 DOM 树,创建真实 DOM 节点
- Patch:比较新旧两棵虚拟 DOM 树,只更新有变化的部分
Vue 的渲染流程
一个 Vue 组件从挂载到更新,经历了三个阶段:
1. 编译
Vue 模板被编译成渲染函数——一个返回虚拟 DOM 树的函数。
这个编译步骤可以在构建时(AOT)提前完成,也可以在浏览器里运行时编译。生产环境一般用构建时编译。
2. Mount
运行时调用渲染函数,拿到虚拟 DOM 树,然后创建真实 DOM。这个过程是响应式的——Vue 会追踪所有用到了的响应式依赖。
3. Patch
当响应式依赖变化时,渲染函数重新执行,生成新的虚拟 DOM 树。然后把新旧两棵树做对比,只把变化应用到真实 DOM。
模板 vs 渲染函数
模板最终也会编译成渲染函数。那为什么不直接写渲染函数?
模板的优势:
| 模板 | 渲染函数 |
|---|---|
| 接近 HTML,易读易写 | 需要嵌套调用 |
| 便于 CSS 和设计师协作 | 有学习成本 |
| 能做静态分析,编译器可优化 | 编译器无法优化 |
渲染函数的优势:
| 模板 | 渲染函数 |
|---|---|
| 灵活性有限 | 完全用 JS 控制 |
| 受限于模板语法 | 任意逻辑都可以 |
日常开发用模板就够了。渲染函数只在高度动态的场景才需要——比如根据运行时配置生成不同结构的组件。
编译器告知的虚拟 DOM
React 等框架的虚拟 DOM 是纯运行时的:每次 re-render 都要全量遍历树、逐个 diff 属性。即使某部分永远不会变,也得重新创建 vnode。
Vue 不同。Vue 同时控制编译器和运行时,编译器可以在编译阶段分析模板,给运行时留下”提示信息”。运行时拿到这些提示后可以跳过很多不必要的工作。
这就是”编译器告知的虚拟 DOM”(Compiler-Informed Virtual DOM)。
优化一:静态缓存
模板里没有动态绑定的部分:
<div>
<div>foo</div> <!-- 静态 -->
<div>bar</div> <!-- 静态 -->
<div>{{ dynamic }}</div>
</div>
foo 和 bar 永远不会变。编译器标记它们为静态,渲染器创建 vnode 后缓存起来,后续 re-render 直接复用同一个 vnode,完全跳过 diff。
连续的静态节点会被合并成一个”静态 vnode”,直接通过 innerHTML 插入,连 vnode 都不用创建。
优化二:Patch Flags
对于有动态绑定的元素,编译器在生成代码时会带上一个”补丁标记”:
createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)
最后的 2 就是 Patch Flag,表示”这个元素只需要更新 class”。
运行时用位运算检查标记:
if (vnode.patchFlag & PatchFlags.CLASS) {
// 只更新 class,其他属性完全不管
}
常见的 Patch Flag:
NotePatch Flag 具体数值为内部实现细节,日常开发中由模板编译器自动生成,无需手动记忆。
| 标记 | 含义 |
|---|---|
1 | 动态文本内容 |
2 | 动态 class |
4 | 动态 style |
8 | 动态属性(除 class/style) |
64 | 稳定子节点顺序的 fragment |
128 | 需要移动子节点的 fragment |
位运算极快。有了 Patch Flag,Vue 更新时只做最少的 diff 工作。
优化三:树拍平(Tree Flattening)
编译器把模板分成一个个”block”——没有 v-if、v-for 等结构指令的区域就是一个 block。
每个 block 内部追踪所有带 Patch Flag 的后代节点,拍平成一个数组:
<div> <!-- root block -->
<div>...</div> <!-- 不追踪 -->
<div :id="id"></div> <!-- 追踪 -->
<div> <!-- 不追踪 -->
<div>{{ bar }}</div> <!-- 追踪 -->
</div>
</div>
re-render 时只遍历这个扁平数组,跳过所有静态节点。静态区域越多,优化越明显。
v-if 和 v-for 会创建新的 block 节点,每个 block 内部独立追踪动态节点。
对 SSR 的优化
Patch Flags 和树拍平同样优化服务端渲染的 hydration 过程:
- 根据 Patch Flag 走快速 hydration 路径
- 只遍历 block 和动态节点,实现”局部 hydration”
本节回顾
- 虚拟 DOM 是用 JS 对象描述 DOM 节点的技术
- Vue 渲染流程:编译 → Mount → Patch
- 模板编译成渲染函数,日常开发推荐模板
- Vue 通过编译器给运行时提供优化提示
- 静态缓存跳过不变节点的 diff
- Patch Flag 告诉运行时该更新什么
- 树拍平把 block 内动态节点拍平成数组,re-render 只遍历数组