Appearance
Vue 的虚拟 DOM、diff 与渲染更新
这篇不准备泛泛去讲“所有前端框架的虚拟 DOM”。
这一篇只聚焦 Vue 自己 的几件事:
- Vue 里的虚拟 DOM 到底是什么
- 为什么 Vue 不是每次都直接手改真实 DOM
- Vue2 和 Vue3 的
render -> vnode -> patch链路是怎么跑的 - Vue3 的
Patch Flag、Block Tree、静态提升到底在优化什么
如果把主线压成一句话:Vue 的虚拟 DOM,本质上是运行时用来描述界面结构和更新差异的一层中间表示;响应式系统负责触发更新,虚拟 DOM 负责把更新更有组织地落到页面。
1. Vue 里的虚拟 DOM 到底是什么
很多人第一次听到“虚拟 DOM”,很容易把它理解成:浏览器 DOM 的完全复制品。
这不准确。
实际写项目时,更多是这样:Vue 的虚拟 DOM 是一棵由 JavaScript 对象组成的 vnode 树,用来描述当前界面应该长成什么样。
例如模板:
vue
<div class="card">
<span>{{ title }}</span>
</div>在 Vue 的运行时语境里,最终会对应成某种 vnode 结构。可以粗略理解成:
ts
const vnode = {
type: 'div',
props: { class: 'card' },
children: [
{
type: 'span',
children: 'hello'
}
]
}这不是 Vue 真实源码里的完整结构,但足够帮助我们建立第一层认识:
type:节点类型props:属性children:子节点
所以把边界放回原位看:虚拟 DOM 不是浏览器提供的对象,而是 Vue 运行时里用来描述视图树的一种 JavaScript 数据结构。
2. 为什么 Vue 不直接每次都操作真实 DOM
这点不要讲成“真实 DOM 一定很慢”这种绝对化结论。
Vue 引入虚拟 DOM,主要是为了统一解决下面几件事:
- 组件更新时,先有一层稳定的视图描述
- 可以在 JavaScript 层比较新旧结构差异
- 最后只把必要的变化落到真实 DOM
- 让模板、渲染函数和运行时更新机制接到一条稳定主线上
也就是说,Vue 不是简单在追求:少改几次 DOM。
它真正想得到的是:一套可组合、可推导、可 patch 的 UI 描述和更新模型。
3. Vue 的渲染链路可以怎么记
不管是 Vue2 还是 Vue3,都可以记这条主线:
- 模板被编译成渲染函数
- 渲染函数执行,生成 vnode 树
- 运行时把 vnode 挂载成真实 DOM
- 状态变化后,再生成新的 vnode 树
- 新旧 vnode 做 diff
- 最后 patch 到真实 DOM
如果压缩成一行,就是:template -> render -> vnode -> patch
4. Vue2 里的虚拟 DOM 和更新流程
Vue2 的整体思路可以拆开来看:
- 模板编译成渲染函数
- 渲染 watcher 执行
_render() _render()生成 vnode_update()把 vnode 挂到页面- 依赖变化后,watcher 再次执行
- 新旧 vnode 进入 patch 流程
4.1 一段源码风格的简化示例
js
/**
* Vue2 渲染更新的简化示意
* 功能:
* - 把组件渲染包装成一个 watcher
* - 每次依赖变化后重新执行 _render 和 _update
* 参数:
* - vm: 当前组件实例
* 返回值:
* - 真实场景里会得到一个渲染 watcher 实例
*/
new Watcher(vm, () => {
// 根据当前状态生成最新 vnode 树
const vnode = vm._render()
// 把 vnode patch 到真实 DOM
vm._update(vnode)
})这段代码最重要的意思是:
- Vue2 的组件渲染本身是被 watcher 驱动的
_render()负责生成虚拟 DOM_update()负责把虚拟 DOM 更新到页面
4.2 Vue2 的 diff 在做什么
当状态变化时,Vue2 不会直接整页重建 DOM。
更常见的流程是:
- 先拿到新的 vnode 树
- 和旧 vnode 树对比
- 找出哪些节点真的变了
- 只 patch 这些变化
所以 Vue2 的关键不是:有了虚拟 DOM 就完全不需要真实 DOM 操作。
而是:真实 DOM 操作被延后到了 patch 阶段,并且尽量只做必要修改。
5. Vue3 里的虚拟 DOM 和更新流程
Vue3 的主线和 Vue2 没有变成另一种完全不同的世界,它仍然遵循:render -> vnode -> patch
但 Vue3 做了两类重要升级:
- 响应式系统升级
- 编译器和虚拟 DOM 更新策略升级
5.1 一段源码风格的简化示例
ts
/**
* Vue3 组件更新的简化示意
* 功能:
* - 重新执行组件渲染逻辑
* - 对比前后 vnode 树
* - 把差异 patch 到页面
* 参数:
* - instance: 当前组件实例
* 返回值:
* - 无显式返回值,副作用是更新 instance.subTree 和真实 DOM
*/
function componentUpdateFn(instance) {
// 生成当前最新的 vnode 树
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// 只把差异 patch 到页面
patch(prevTree, nextTree)
}这段代码最重要的意思是:
- Vue3 仍然是 vnode diff + patch 这条主线
- 组件更新时会拿新旧两棵树做对比
- 运行时最终还是要把差异落到真实 DOM
6. Vue3 比 Vue2 在虚拟 DOM 层到底强在哪
最容易讲偏的一点,是把答案只说成:Vue3 更快。
这不够。
Vue3 在虚拟 DOM 相关层面的升级,主要不是“diff 算法换了个魔法版本”,而是:
编译器提前告诉运行时哪些节点会变、哪些节点不会变,从而让 patch 更有针对性。
这就是为什么 Vue3 的虚拟 DOM 优化,必须连着编译器一起理解。
7. Patch Flag 是什么
Patch Flag 可以看成 编译阶段给 vnode 打上的“更新提示标签”。
它在回答:这个节点下一次更新时,真正可能变化的部分是什么?
例如有的节点:
- 只有文本会变
- 只有 class 会变
- 只有 style 会变
- 某些 props 会变
那运行时 patch 时,就不必“把整块再仔细检查一遍”,而是可以更有针对性。
7.1 一个很粗略的理解示意
ts
const vnode = createVNode('div', { class: cls }, text, PatchFlags.TEXT)这段不是要你背 API,而是想表达:
- 编译器已经提前分析出“这个节点主要是文本在变”
- 运行时 patch 就能少做一些无意义检查
8. Block Tree 是什么
Block Tree 这个词看起来有点抽象。
可以把它理解成:Vue3 会把模板里真正可能发生动态变化的节点尽量圈出来,形成一棵更关注动态部分的结构。
它想解决的问题是:更新时,不要每次都把整棵树从头到尾按同样力度检查。
所以把话收回来:
- 普通 vnode 树描述的是整体结构
Block Tree更强调“动态节点集合”
它和 Patch Flag 一起,构成了 Vue3 虚拟 DOM 更新更细粒度的基础。
9. 静态提升在优化什么
静态提升 可以看成 把那些完全不会变的节点,在编译阶段就提出来复用。
这样做的价值是:
- 不必在每次渲染时都重新创建这部分 vnode
- patch 时也能更快跳过稳定部分
例如:
vue
<div>
<h1>固定标题</h1>
<span>{{ count }}</span>
</div>这里的 h1 如果内容完全静态,就不必每次更新 count 时都重新按动态节点处理。
10. Vue2 和 Vue3 在虚拟 DOM 层的核心差异
可以用一张表压缩理解:
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 基本主线 | render -> vnode -> patch | render -> vnode -> patch |
| 更新触发 | watcher | effect + scheduler |
| 编译提示 | 相对较弱 | 更积极 |
| 动态节点识别 | 运行时承担更多 | 编译阶段承担更多 |
| patch 粒度 | 更偏经典模式 | 更细粒度 |
所以实际写项目时,更多是这样:Vue2 和 Vue3 都用虚拟 DOM,但 Vue3 更擅长把“哪些节点真的会变”这件事前移到编译阶段。
11. 一张总图看清 Vue 的虚拟 DOM 主线
mermaid
flowchart TD
A[模板 template] --> B[编译成 render 函数]
B --> C[执行 render 生成 vnode 树]
C --> D[首次 patch 挂载成真实 DOM]
E[响应式状态变化] --> F[重新执行 render]
F --> G[生成新 vnode 树]
G --> H[与旧 vnode diff]
H --> I[patch 到真实 DOM]
J[Vue3 编译优化] --> K[Patch Flag]
J --> L[Block Tree]
J --> M[静态提升]
K --> H
L --> H
M --> H12. 一段更贴近 Vue3 编译优化思路的简化示意
ts
/**
* 这是“思路示意”,不是完整源码
* 功能:
* - 编译阶段把动态节点信息提前挂到 vnode 上
* 参数:
* - type: 节点类型
* - props: 节点属性
* - children: 子节点
* - patchFlag: 当前节点的动态提示信息
* 返回值:
* - 一个带有动态更新提示的 vnode
*/
function createVNode(type, props, children, patchFlag) {
return {
type,
props,
children,
patchFlag
}
}这段代码想表达的不是具体实现细节,而是:Vue3 的 vnode 不只是“结构描述”,还可能带着编译阶段提前分析出来的更新提示。
13. 工程里最容易讲偏的地方
最常见的问题包括:
- 把虚拟 DOM 说成“真实 DOM 的完整替身”
- 只会说“虚拟 DOM 快”
- 只讲响应式,不讲
render -> vnode -> patch - 只讲运行时,不讲 Vue3 编译优化
- 把 Vue 的虚拟 DOM 和别的框架混着讲,最后丢掉 Vue 自己的重点
14. 一份检查清单
当你回答 Vue 的虚拟 DOM 时,至少可以问自己:
- 我有没有讲清 vnode 是什么
- 我有没有讲清
render -> vnode -> patch - 我有没有说明 Vue2 和 Vue3 都在走这条主线
- 我有没有讲出 Vue3 的优势来自编译器 + 运行时配合
- 我有没有把答案收口在 Vue 本身,而不是泛讲所有框架
15. 小结
如果把这篇压缩成一句话,可以记住:
Vue 的虚拟 DOM,是 Vue 运行时用来描述界面和组织更新的一层 vnode 结构;Vue2 和 Vue3 都走 render -> vnode -> patch 这条主线,而 Vue3 的优势在于编译器会提前分析动态节点,让 patch 更有针对性。