虚拟 DOM 与 Diff 算法深度剖析
虚拟 DOM (Virtual DOM) 和 Diff 算法是 Vue 能够高效渲染的核心机制。
1. 虚拟 DOM 本质
虚拟 DOM 仅仅是一个普通的 JavaScript 对象 (VNode),用来描述真实 DOM 的结构。
javascript
const vnode = {
tag: "div",
props: { id: "app", class: "container" },
children: [
{ tag: "h1", children: "Hello Vue" },
{ tag: "p", children: "Content..." },
],
};优点:
- 性能优化: 将多次 DOM 操作合并为一次 (Batch Update)。
- 跨平台: VNode 不依赖浏览器环境,可以渲染到 Weex, 小程序, SSR 等。
2. Diff 算法详解
Diff 算法的目的是找出新旧 VNode 之间的差异,并最小化 DOM 操作。
2.1 比较策略 (优化假设)
为了将复杂度从 O(n^3) 降低到 O(n),Vue 做了一些假设:
- 同层比较: 只比较同一层级的节点,不跨层比较。
- 类型检测: 如果标签名不同,直接销毁旧节点,创建新节点 (不再深入比较子节点)。
- Key 标识: 使用
key来复用同一层级的节点。
2.2 Vue 2: 双端 Diff (Double-ended Diff)
Vue 2 采用四个指针,从两端向中间进行比较。
oldStartIdx/oldEndIdxnewStartIdx/newEndIdx
比较流程:
- 头头比较:
oldStartvsnewStart(相同则 patch,索引右移)。 - 尾尾比较:
oldEndvsnewEnd(相同则 patch,索引左移)。 - 头尾比较:
oldStartvsnewEnd(相同则 patch,将oldStart移动到oldEnd后面)。 - 尾头比较:
oldEndvsnewStart(相同则 patch,将oldEnd移动到oldStart前面)。 - Key 查找: 如果以上都不中,拿
newStart的 key 去oldCh的 map 中查找。找到则移动,没找到则新建。
2.3 Vue 3: 快速 Diff (Fast Diff)
Vue 3 引入了 最长递增子序列 (Longest Increasing Subsequence, LIS) 算法,进一步减少 DOM 移动操作。
流程:
- 预处理: 处理头部和尾部相同的节点 (Sync from start/end)。
- 剩余节点处理:
- 如果新节点已遍历完,旧节点还有剩 -> 删除旧节点。
- 如果旧节点已遍历完,新节点还有剩 -> 挂载新节点。
- 乱序部分:
- 建立新节点的
key:index映射。 - 初始化
newIndexToOldIndexMap数组。 - 遍历旧节点,填充 map,标记是否需要移动 (
moved标志)。 - 如果需要移动,计算 LIS。LIS 对应的节点保持不动,其他节点根据 LIS 结果进行移动或插入。
- 建立新节点的
3. PatchFlag (Vue 3 编译优化)
Vue 2 的 Diff 是全量比较,即使是静态节点也会参与比较。 Vue 3 在编译阶段对动态节点进行了标记 (PatchFlag)。
javascript
// 编译前
<div>
<span>static</span>
<span :id="id">{{ msg }}</span>
</div>
// 编译后的 VNode (伪代码)
{
type: 'div',
children: [
{ type: 'span', children: 'static' }, // 静态节点
{
type: 'span',
children: ctx.msg,
props: { id: ctx.id },
patchFlag: 9 // TEXT + PROPS
}
]
}运行时优化:
- Diff 时,如果遇到静态节点 (没有 PatchFlag),直接跳过。
- 如果有 PatchFlag,根据 Flag 的值仅比较对应的内容 (如只比较 Text,或只比较 Props)。
- Block Tree: 将模版切分为 Block,配合 PatchFlag,Diff 速度与动态节点数量相关,与模板大小无关。
4. Key 的重要性
key 是 VNode 的唯一标识。
为什么不推荐用 index 作为 key?
- 性能浪费: 如果在列表头部插入一个元素,会导致所有元素的 index 发生变化,Vue 会认为所有元素都变了,导致全部重新渲染 (虽然复用了 DOM,但子组件状态可能错乱)。
- 状态错乱: 如果列表中包含有状态的组件 (如输入框),使用 index 做 key 会导致输入框的内容保留在错误的位置 (就地复用策略)。
结论: 始终使用后端返回的唯一 ID (如 id, uuid) 作为 key。
