Diff算法对比:Vue3 vs React 深度解析
Vue3 Diff 是双端比较四指针夹击,优先匹配头头尾尾头尾尾头;React Diff 是单向遍历 Fiber 链表,key 匹配复用不匹配跳出。 Vue 双端在列表反转场景占优,React 简洁换来可维护性与 Fiber 兼容,面试常考两者策略差异与各自适用场景。
一句话概括
Vue 3 的 Diff 是双端比较——四指针从数组两端夹击,优先匹配头头/尾尾/头尾/尾头四种快速路径;React 的 Diff 是单向遍历——从 Fiber 链表头部开始,key 匹配就复用、不匹配就跳出。Vue 的双端优势在列表反转这类场景明显,React 的简洁换来代码可维护性和 Fiber 架构兼容性。
核心知识点
1. Vue 3 双端比较:四个指针夹击
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Vue 3 双端 Diff 简化核心
function patchKeyedChildren(c1, c2) {
let i = 0; // 头指针
let e1 = c1.length - 1; // 旧数组尾指针
let e2 = c2.length - 1; // 新数组尾指针
// ① 头头比较:新旧头部相同 → 复用,i++
while (i <= e1 && i <= e2 && c1[i].key === c2[i].key) {
patch(c1[i], c2[i]);
i++;
}
// ② 尾尾比较:新旧尾部相同 → 复用,e1--, e2--
while (i <= e1 && i <= e2 && c1[e1].key === c2[e2].key) {
patch(c1[e1], c2[e2]);
e1--; e2--;
}
// ③ 头尾比较:旧头 vs 新尾(节点被移到最后)
// ④ 尾头比较:旧尾 vs 新头(节点被移到最前)
// ⑤ 剩余用 key → index 映射 + LIS 算最少移动
}
四种快速路径覆盖了最常见的列表变化——追加、删除、反转、移动。第五步才用 LIS 算法处理复杂乱序。
2. React 单向遍历:到 key 不同就跳出
1
2
3
4
5
6
7
8
// React 第一轮(相当于 Vue 的头头比较,但更"脆弱")
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) break; // key 不同 → 立刻跳出!
// key 匹配 → 复用,继续下一个
oldFiber = oldFiber.sibling;
}
// 跳出后剩余节点进第二轮 Map 匹配
React 没有双端能力,因为 Fiber 是单向链表——没有 prev 指针,无法从尾部向前遍历。
3. 列表反转场景的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
旧: A B C D E
新: E D C B A
Vue 3:
头头: A vs E → 不匹配
尾尾: E vs A → 不匹配
头尾: A vs A → 匹配!(A 被移到末尾)i++, e2--
尾头: E vs E → 匹配!(E 被移到开头)e1--, i++
B vs D → 头尾匹配;D vs B → 尾头匹配
共 4 次移动
React:
第一轮: A vs E → key 不同,break
剩余旧: [A,B,C,D,E],剩余新: [E,D,C,B,A]
第二轮 Map 匹配: E(4)不移, D(3)移动, C(2)移动, B(1)移动, A(0)移动
共 4 次移动
这个场景结果一样。但如果中间有插入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
旧: A B C D
新: A X B C D
Vue 3: 头头 A 匹配 → 剩余 [B,C,D] vs [X,B,C,D]
→ 构建 keyToIndex: {B:0,C:1,D:2}
→ X 不在 map 中 → 新建
→ B C D 的索引递增 → 无需移动
共 1 次新建
React: A 匹配 → B vs X → key 不同 → break
→ Map 中查 X → 找不到 → 新建
→ 查 B → 找到 → oldIndex=1 >= lastPlacedIndex(0) → 不移
→ C D 同理
共 1 次新建
日常场景两者差距微乎其微,远没有理论上的显著差异。
4. 最长递增子序列(LIS)
1
2
3
4
5
6
7
// Vue 3 用 LIS 算最少移动次数
function getSequence(arr) {
// 返回 arr 中"最长递增子序列"的索引数组
// 例如 [4,3,2,1] → LIS 长度 1, 索引 [0]
// [1,3,2,4] → LIS 长度 3, 索引 [0,1,3]
// LIS 中的元素保持不变,其余的需要移动
}
React 用贪心 lastPlacedIndex 近似 LIS,在绝大多数场景结果一致,极端情况可能多 1-2 次 DOM 操作。这是工程取舍:代码复杂度降低 > 极偶尔的多一次移动。
5. key 在两者中的处理
1
2
3
4
5
6
Vue 3: key 用于双端匹配的快速判断,以及 LIS 阶段的新旧映射
React: key 用于第一轮的 updateSlot 匹配,以及第二轮的 Map 查找
共同点:无 key = 灾难。
- Vue 3: 双端比较退化为顺序比较,大量错误复用
- React: Map 用 index 做 key,删除中间项后全部错位
其实你每天都在用
- 追加/删除尾部:两者的头头/尾尾比较都能在第一轮处理,路径最短
- 反转/拖拽排序:Vue 的双端有天然优势,React 依赖第二轮 Map 才能处理
- 表格按列排序:列表顺序全变,两者都不会在第一轮走完,都会进入中间处理阶段
- key 是 ID vs 是 index:不管哪个框架,用 index 做 key 在删除/插入时都会导致大量错位复用
常见误解(FAQ)
❌ 误区:「双端 Diff 总是比单端 Diff 快」 不是。头头+尾尾的快速路径确实好,但如果变化不在两端(如纯中间插入),双端多做的那几次比较就是纯开销。实际场景差距在 5% 以内。
❌ 误区:「React 不用双端是因为水平不行」 是数据结构限制。Fiber 是单向链表,没有 prev 指针,无法从尾部向前遍历。React 团队选择了”更简单的更新模型换取可中断的并发渲染”这个更大目标。
❌ 误区:「虚拟 DOM Diff 比直接操作 DOM 快」 经典迷思。直接
el.textContent = 'new'比走虚拟 DOM 创建、Diff、最终 patch 快得多。虚拟 DOM 的价值是声明式编程体验 + 批量 DOM 操作优化,不是单次操作速度。❌ 误区:「两个框架的 Diff 都能跨层级比较」 都不能。同层级比较是虚拟 DOM Diff 的性能基础假设。跨层级移动元素(如从 body 移到 sidebar),两者都会销毁重建。
一句话总结
Vue 3 双端 Diff 像”两根筷子从两头夹”,React 单向 Diff 像”一根筷子从头拨”——速度差不多,脸不同而已。真正影响性能的不是 Diff 算法,而是你有没有给列表写对 key、有没有用虚拟滚动控制 DOM 数量。