文章目录
- 前言
- [一、什么是虚拟 DOM](#一、什么是虚拟 DOM)
-
- [1.1 定义](#1.1 定义)
- [1.2 为什么需要](#1.2 为什么需要)
- [1.3 并非总是更快](#1.3 并非总是更快)
- [二、VNode 结构](#二、VNode 结构)
-
- [2.1 基本字段](#2.1 基本字段)
- [2.2 常见类型](#2.2 常见类型)
- 三、更新流程
- [四、Vue 2 双端 Diff](#四、Vue 2 双端 Diff)
-
- [4.1 算法思路](#4.1 算法思路)
- [4.2 特点](#4.2 特点)
- [五、Vue 3 快速 Diff](#五、Vue 3 快速 Diff)
-
- [5.1 为什么放弃双端 Diff](#5.1 为什么放弃双端 Diff)
- [5.2 快速 Diff 流程(列表)](#5.2 快速 Diff 流程(列表))
- [5.3 最长递增子序列(LIS)](#5.3 最长递增子序列(LIS))
- [六、Vue 3 编译时优化(概览)](#六、Vue 3 编译时优化(概览))
-
- [6.1 PatchFlag:标记动态节点](#6.1 PatchFlag:标记动态节点)
- [6.2 Block Tree:扁平化动态节点](#6.2 Block Tree:扁平化动态节点)
- [6.3 静态提升](#6.3 静态提升)
- [七、key 与 Diff 的关系(简述)](#七、key 与 Diff 的关系(简述))
- [八、与 React 的对比](#八、与 React 的对比)
- 九、面试聚焦
-
- [9.1 虚拟 DOM 并非总是更快](#9.1 虚拟 DOM 并非总是更快)
- [9.2 Vue 3 为什么改 Diff?](#9.2 Vue 3 为什么改 Diff?)
- [9.3 PatchFlag 做什么?](#9.3 PatchFlag 做什么?)
- [9.4 Block Tree 是什么?](#9.4 Block Tree 是什么?)
- 十、易混淆点
- 十一、思考与练习
- 总结
前言
虚拟 DOM 是 Vue 渲染层的核心机制:用 JavaScript 对象描述 DOM,通过 Diff 算法找出最小变更并批量更新。本篇会讲清楚:
- VNode 结构与更新流程
- Vue 2 双端 Diff vs Vue 3 快速 Diff
- 最长递增子序列(LIS)与编译时优化概览
一、什么是虚拟 DOM
1.1 定义
虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级表示。状态变化时生成新的 VNode 树,与旧树 Diff 后,只把差异应用到真实 DOM。
javascript
// 真实 DOM
<div class="box">
<h1>Hello</h1>
</div>
// 虚拟 DOM(VNode 示意)
const vnode = {
type: 'div',
props: { class: 'box' },
children: [
{ type: 'h1', children: 'Hello' }
]
}
1.2 为什么需要
| 问题 | 虚拟 DOM 的解决 |
|---|---|
| 直接操作 DOM 慢且难追踪 | 声明式描述 UI,框架算最优更新 |
| 多次状态变更多次 DOM 操作 | 合并为一次批量更新 |
| 跨平台(Web / SSR / 小程序) | 同一套 VNode 可对接不同渲染器 |
1.3 并非总是更快
极少量的 DOM 更新时,手动改 DOM 可能更快。虚拟 DOM 的价值在于复杂场景下的声明式开发、批量更新和跨平台抽象,而不是「一定比原生 DOM 快」。
二、VNode 结构
2.1 基本字段
javascript
const vnode = {
type: 'div', // 标签名、组件、Fragment 等
props: { class: 'box' },
children: [],
key: 'unique-id', // 列表 Diff 用
el: null, // 运行时关联的真实 DOM
patchFlag: 0, // Vue 3 编译时标记(见下文)
dynamicChildren: null // Vue 3 Block Tree 动态子节点
}
2.2 常见类型
javascript
// 文本
{ type: Text, children: 'hello' }
// 元素
{ type: 'div', props: {}, children: [] }
// 组件
{ type: MyComponent, props: { msg: 'hi' } }
// Fragment(多根节点)
{ type: Fragment, children: [...] }
三、更新流程
响应式数据变化
↓
触发组件 re-render,生成新 VNode 树
↓
新旧 VNode 树 Diff(patch)
↓
计算出最小变更集(增删改移)
↓
批量应用到真实 DOM
javascript
// 简化 Diff 思路
function patch(oldVNode, newVNode) {
if (oldVNode.type !== newVNode.type) {
// 类型不同 → 替换节点
replaceNode(oldVNode, newVNode)
return
}
// 同类型 → 比 props、比 children
patchProps(oldVNode, newVNode)
patchChildren(oldVNode, newVNode)
}
四、Vue 2 双端 Diff
4.1 算法思路
Vue 2 对同级列表 使用双端比较:新旧数组各设头尾指针,从两端向中间同时比较,尽量复用 DOM。
旧: A B C D
↑ ↑
oldStart oldEnd
新: D A B C
↑ ↑
newStart newEnd
比较顺序(共 4 种):
1. oldStart vs newStart
2. oldEnd vs newEnd
3. oldStart vs newEnd
4. oldEnd vs newStart
都不匹配 → 用 key 在旧列表中查找
4.2 特点
- 适合列表头尾增删、反转等常见场景
- 依赖 key 做节点身份识别
- 最坏情况仍需较多比较,Vue 3 做了进一步优化
五、Vue 3 快速 Diff
5.1 为什么放弃双端 Diff
Vue 3 借鉴 Inferno 的快速 Diff,目标:
- 减少不必要的节点比较次数
- 用最长递增子序列(LIS)优化列表移动操作,少做 DOM insert/move
5.2 快速 Diff 流程(列表)
1. 从头同步:新旧节点 type + key 相同则 patch,不同则停
2. 从尾同步:同上,从尾部向前
3. 中间段:用 key → index 映射处理新增、删除、移动
4. 移动优化:对需要移动的节点求 LIS,LIS 内节点不移动,其余按需 insert
5.3 最长递增子序列(LIS)
旧: [A, B, C, D, E]
新: [A, C, D, B, E] (B 从 index 1 移到 index 3)
需要移动的: B
LIS 帮助找出「已经相对有序、不必动」的节点,减少 DOM 移动次数
LIS 让 Diff 在「乱序但可复用」的列表里,用最少 DOM 移动完成更新。
六、Vue 3 编译时优化(概览)
Diff 之外,Vue 3 在编译阶段减少需要 Diff 的节点量:
6.1 PatchFlag:标记动态节点
编译器分析模板,给 VNode 打上「哪里会变」的标记:
javascript
// 编译结果示意
createElementVNode('div', { class: 'static' },
createTextVNode('hello', PatchFlags.TEXT) // 仅文本会变
)
| PatchFlag | 含义 |
|---|---|
| TEXT (1) | 动态文本 |
| CLASS (2) | 动态 class |
| STYLE (4) | 动态 style |
| PROPS (8) | 动态 props |
| ... | 组合标记 |
Diff 时若 patchFlag 为 0,可跳过该节点子树比较;有标记则只比较标记部分。
6.2 Block Tree:扁平化动态节点
将模板中的动态节点收集到 dynamicChildren 数组,Diff 时只遍历动态节点,静态子树整段跳过。
vue
<div>
<p>静态标题</p> <!-- 静态,不参与 Diff -->
<p>{{ msg }}</p> <!-- 动态,进入 dynamicChildren -->
<span>{{ count }}</span> <!-- 动态,进入 dynamicChildren -->
</div>
6.3 静态提升
纯静态节点提升到 render 函数外,只创建一次,后续 render 直接复用,避免重复生成 VNode。
编译优化的细节(PatchFlag 类型、Block 收集规则等)在编译优化专题中展开。
七、key 与 Diff 的关系(简述)
列表 Diff 依赖 key 判断「同一节点」:
- 相同 key + 相同 type → patch(复用 DOM)
- 不同 key → 销毁旧节点,创建新节点
key 不稳定(如用 index 做增删)会导致错误复用。key 的完整原理见专题「Key 的作用与原理」。
八、与 React 的对比
| 对比项 | Vue 3 | React |
|---|---|---|
| Diff 粒度 | 组件级 + Block 内动态节点 | Fiber 可中断的增量 Diff |
| 列表算法 | 快速 Diff + LIS | 单端 + key 映射 |
| 编译优化 | PatchFlag、静态提升、Block Tree | 部分优化,策略不同 |
两者都用虚拟 DOM,但 Diff 策略和编译优化路径不同。
九、面试聚焦
9.1 虚拟 DOM 并非总是更快
简单场景手动 DOM 可能更快;虚拟 DOM 的价值是声明式、批量更新、跨平台。
9.2 Vue 3 为什么改 Diff?
双端 Diff 在复杂列表下比较次数仍偏多;快速 Diff + LIS 减少移动,配合 PatchFlag / Block Tree 减少比较范围。
9.3 PatchFlag 做什么?
编译期标记 VNode 哪些部分动态,Diff 时跳过静态内容,只更新标记位。
9.4 Block Tree 是什么?
把动态节点扁平收集,Diff 只比 dynamicChildren,静态子树整段跳过。
十、易混淆点
- 虚拟 DOM ≠ 更快:是开发模型和批量更新策略,不是性能银弹。
- Diff 只做同级比较:不会跨层级移动节点(O(n) 层级比较)。
- Vue 2 / Vue 3 列表 Diff 不同:Vue 3 用快速 Diff + LIS。
- PatchFlag 是编译产物:手写 render 函数默认无此优化。
- Shadow DOM ≠ Virtual DOM:前者是 Web Components 浏览器封装,与框架 VNode 无关。
十一、思考与练习
1. 虚拟 DOM 的更新流程是什么?
解析:数据变 → 新 VNode → 与旧 VNode Diff → 最小变更 → 更新真实 DOM。
2. Vue 2 和 Vue 3 列表 Diff 有何不同?
解析:Vue 2 双端比较;Vue 3 快速 Diff,头尾同步 + 中间 key 映射 + LIS 优化移动。
3. LIS 在 Diff 中的作用?
解析:找出相对有序、不必移动的节点,减少 DOM insert/move 次数。
4. PatchFlag 和 Block Tree 解决什么问题?
解析:减少 Diff 范围------只比动态节点、只更新标记为动态的部分,静态内容跳过。
5. 为什么说虚拟 DOM 不总是更快?
解析:创建 VNode 和 Diff 本身有开销;极简单更新直接改 DOM 可能更省。
总结
- 虚拟 DOM:JS 对象描述 DOM,声明式 + 批量更新 + 跨平台
- 更新流程:新 VNode → Diff → 最小 patch → 真实 DOM
- Vue 2:双端 Diff,依赖 key
- Vue 3:快速 Diff + LIS;PatchFlag、Block Tree、静态提升减少 Diff 量
- 本质:虚拟 DOM 是工程权衡,不是「一定比原生 DOM 快」