口述Vue3 Diff完整过程
一句话概括
Vue 3 的 Diff 算法 = 预处理(头尾同步) + 最长递增子序列(LIS)最小移动,五步走完:头部同步 → 尾部同步 → 纯增删判断 → key 映射 → LIS 决定谁动谁不动,实际场景接近 O(n)。
核心知识点
1. Diff 五步流程速览
1
2
3
4
5
6
7
8
旧: [A, B, C, D, E, F]
新: [A, B, D, E, C, G]
① 头部同步:A==A, B==B → i=2
② 尾部同步:F≠G, E≠G... 不匹配立即停(e1=5, e2=5)
③ 判断:i不>e1 且 j不>e2 → 非纯增删,进复杂阶段
④ key映射:剩余[C,D,E,F]→[D,E,C,G],构造位置数组 [1,2,0,3]
⑤ LIS: [1,2]→D和E不动,C移动、F删除、G新增
2. 为什么 LIS 能保证最少移动
newIndexToOldIndexMap 记录了「旧节点在新列表中的位置」。如果这个数组中的值呈现递增趋势,说明这些节点在新旧列表中的相对顺序一致——天然不需要动。找到最长的那个递增子序列,就是「不动节点」的最大集合。
1
2
3
4
5
6
7
8
9
10
11
12
// newIndexToOldIndexMap = [2, 4, 0, 1, 5, 0]
// LIS 索引 = [0, 1, 4] → 索引 0、1、4 的节点保持不动
// 逆序遍历:不在 LIS 中的 → mount(新增) 或 move(插入到锚点前)
for (let i = toBePatched - 1; i >= 0; i--) {
if (newIndexToOldIndexMap[i] === 0) {
mount(newChild); // 纯新增
} else if (i !== lis[lisPtr]) {
insertBefore(newChild.el, anchor); // 移动
} else {
lisPtr--; // 不动
}
}
从后往前遍历是关键:保证每次插入时
anchor(参考节点)已经在正确位置上了。
3. Vue 3 的 Block Tree 编译优化
Vue 3 Diff 比 Vue 2 快的关键不只在于 LIS,更在于编译阶段就把静态内容标出来了。模板编译后形成 Block Tree,每个 Block 只追踪自己的动态节点(通过 PatchFlag)。静态内容不参与 Diff,运行时直接跳过。
1
2
3
4
5
<div> <!-- Block -->
<span>静态文本</span> <!-- 不参与 Diff -->
<span>{{ msg }}</span> <!-- PatchFlag=TEXT,只比文本 -->
<p :class="cls">...</p> <!-- PatchFlag=CLASS,只比 class -->
</div>
4. 没有 key 时走什么路径
不传 key → patchUnkeyedChildren:按索引逐一 patch,旧长新短删多余、新长旧短加多余。O(n) 很快,但列表重排序时会复用到错误的 DOM。永远传 key。
1
2
3
4
5
6
function patchUnkeyedChildren(c1, c2, ...) {
const common = Math.min(c1.length, c2.length);
for (let i = 0; i < common; i++) patch(c1[i], c2[i], ...); // 按位置硬匹配
if (c1.length > c2.length) unmount(common...);
else mount(c2.slice(common)...);
}
5. Vue 3 Diff 最坏场景是什么
所有节点都打乱且 key 不固定时,退化为每个旧节点都去新列表搜索匹配(findIdxInOld),变成 O(n²)。但实际项目中有稳定的 key 和 Block Tree 优化,几乎碰不到这个情况。
「其实你每天都在用」
- v-for 列表更新:添加/删除/排序列表项时,Vue 3 背后就在跑这五步 Diff。
- 表格排序:点击列头排序时,
v-for的数组变了,Diff 用 LIS 找出哪些行可以不动。 - 拖拽排序:拖拽改变数组顺序,LIS 保证只移动真正需要动的少数卡片。
- 搜索过滤:输入关键词改变
computed结果,新旧列表差异可能是「全部不同」,Diff 退化为全删全建,只在编译优化的动态节点范围里做。 - 虚拟列表数据刷新:滚动加载新数据时,增量 Diff 决定新增/回收哪些 DOM 节点。
常见误解(FAQ)
❌ 误区:Vue 3 Diff = Vue 2 双端比较。 Vue 2 是四路双端比较(新头旧头、新尾旧尾、新头旧尾、新尾旧头),Vue 3 只用两路(头同步、尾同步),中间乱序部分交给 LIS。完全不同的策略。
❌ 误区:只有乱序时 LIS 才有用。 头部/尾部同步阶段就已经消化掉了大部分未变化的节点(如尾部追加、头部插入),LIS 只在少数复杂场景才派上用场。日常开发中大部分 Diff 在头尾同步就结束了。
❌ 误区:LIS 让 Vue 3 Diff 永远比 React 快。 两回事。React 的 Fiber Diff 是单向遍历+移动标记,不做 LIS。React 团队认为 LIS 的 O(n log n) 计算开销不值得——DOM 的 insertBefore 已经很快。这是两种哲学:Vue 用算力换更少 DOM 操作,React 接受多几次移动来精简 Diff 逻辑。
❌ 误区:Vue 3 Diff 是完全 O(n) 的。 最坏情况下退化为 O(n²)(所有节点都需遍历查找匹配),实际配合 key 和 Block Tree 接近 O(n)。但理论上不是严格的 O(n) 算法。
一句话总结
Vue 3 的 Diff 不是炫技——「头尾消化掉 90% 的常见更新 + LIS 兜底剩下的乱序」,是经过大量 benchmark 验证过的务实之选。理解它,你就明白为什么 v-for 一定要传 key、为什么列表操作比你想象的便宜。