口述:React Diff完整过程
口述:React Diff完整过程
一句话概括
React Diff 是两次判断 + 三轮遍历的协作:先判断子元素是单个还是数组,单元素走两步(key → type),数组走三轮(线性扫描 → Map 匹配 → 删除残余),全程贯彻”能复用绝不重建”的最低 DOM 操作原则。
核心知识点
1. 入口分发:newChild 决定了走哪条路
1
2
3
4
5
6
7
8
9
10
11
12
13
function reconcileChildren(workInProgress, newChild) {
// 首次挂载没有 Diff,直接创建
if (current === null) return mountChildFibers(...);
// 更新阶段:根据 newChild 类型分发
if (typeof newChild === 'object' && newChild.$$typeof)
return reconcileSingleElement(...); // <div/> → 单节点 Diff
if (typeof newChild === 'string')
return reconcileSingleTextNode(...); // "hello" → 文本 Diff
if (Array.isArray(newChild))
return reconcileChildrenArray(...); // [<A/>,<B/>] → 多节点 Diff
// null/false → 删除所有旧子节点
}
2. 单节点 Diff:key 过不了就删,type 过不了也删
1
2
3
4
5
6
7
8
旧 Fiber 存在?
├─ 不存在 → 直接创建新 Fiber
└─ 存在
├─ key 不同 → deleteChild → 检查旧 Fiber 的下一个 sibling
│ (继续循环,直到 key 匹配或用完旧节点)
└─ key 相同
├─ type 相同 → useFiber! 复用,只更新 props
└─ type 不同 → deleteChild + createFiber,必须重建
特别注意:key 匹配后如果旧 fiber 链表还有 sibling(之前是列表),要 deleteRemainingChildren 全删。
3. 多节点 Diff 三轮遍历(面试口述模板)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
第一轮:从左到右逐个比 key
for oldFiber 和 newElement 同时存在:
key 匹配且 type 匹配 → 复用,继续
key 匹配但 type 不匹配 → 删旧建新,继续
key 不匹配 → break!(立刻跳出第一轮)
跳出后判断:
oldFiber === null → 剩余新节点全部新增
newIdx === newChildren.length → 剩余旧节点全部删除
两者都有 → 进入第二轮
第二轮:Map + lastPlacedIndex
① 剩余旧节点 → new Map(key → fiber)
② 遍历剩余新节点:
在 Map 中 key 查找
→ 找到且 type 匹配: 复用,lastPlacedIndex 判断是否移动
→ 找到但 type 不同: 删旧建新
→ 没找到: 新建
第三轮:Map 中剩余的旧节点 → 全部删除(打 Deletion flag)
4. 手算 lastPlacedIndex
1
2
3
4
5
6
7
8
9
10
11
旧: A(0) B(1) C(2) D(3) E(4)
新: A E D C B
lastPlacedIndex = 0
A: oldIndex=0 >= 0 → 不移, lastPlacedIndex=0
E: oldIndex=4 >= 0 → 不移, lastPlacedIndex=4
D: oldIndex=3 < 4 → 移动 🔄
C: oldIndex=2 < 4 → 移动 🔄
B: oldIndex=1 < 4 → 移动 🔄
不复用0,移动3个。如果 "E" 也不复用就是 0 复用全移动。
5. 首次挂载为什么不需要 Diff
1
2
3
4
5
6
// mount 阶段 shouldTrackSideEffects = false
// 所有节点直接创建,不标记删除/移动 flag
// 因为根本没有旧 DOM 需要清理
function mountChildFibers(...) {
return reconcileChildFibersImpl(..., false); // false = 不追踪副作用
}
其实你每天都在用
- 列表追加:
[...items, newItem]追加到底部,第一轮全部匹配,直接跳出,O(n) 搞定,不会进入第二三轮 - 条件渲染切换:
{show ? <A/> : <B/>}→ 单节点 Diff,key 相同 type 不同 → 删了重建 - 搜索过滤:
items.filter(...)结果变小,第一轮匹配几个后新节点用完 → 剩余旧节点全删(不进入第二轮,走出口判断快车道) - key 是 ID vs key 是 index:用数据库 ID 做 key,Map 能精确匹配到对应旧节点;用 index 做 key 删除中间项后所有后续项内容全错位
常见误解
❌ 误区:「Diff 每次都要走完三轮」 不是。末尾追加、末尾删除这些场景第一轮就走到底了(oldFiber 或 newElement 用完),直接走出口判断跳过二三两轮。
❌ 误区:「单节点 Diff 只比较一个旧 fiber」 错。如果 key 不匹配,React 会沿着
oldFiber.sibling链表逐个比较,直到找到 key 匹配或链表走完。❌ 误区:「Diff 结果直接影响 DOM」 不。Diff 只打 flag(Placement/Deletion/Update),真正的 DOM 操作在 commit 阶段执行。两个阶段严格分离是 Fiber 架构的核心设计。
❌ 误区:「React 和 Vue 的 Diff 是一样的」 本质不同。React 是单向链表 → 只支持从左到右扫描,Vue 3 是数组 → 支持双端比较(头头、尾尾、头尾、尾头四种匹配)。Vue 的双端比较在某些场景下更高效。
一句话总结
Diff 的本质是把”新老两棵树找最小差异”的 NP 难题降级为 O(n) 的工程近似解——通过三个假设(同级比较、type 不同即重建、key 标识)换取可接受的性能和开发体验。
本文由作者按照 CC BY 4.0 进行授权