单节点Diff算法深度解析
一句话概括
单节点 Diff 的规则很简单:先比 key(是不是同一个身份),再比 type(能不能复用 DOM)——两者都匹配就复用只改属性,有一项不匹配就销毁重建。最多一次 DOM 操作。
核心知识点
1. 判断流程:三次决策
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ReactFiberChild.js 源码简化
function reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes) {
const key = newChild.key; // 没指定 key 时默认 null
let child = currentFirstChild;
while (child) {
// 决策①:key 是否相同?
if (child.key !== key) {
deleteChild(returnFiber, child); // 不同 → 删掉当前旧节点
child = child.sibling; // → 继续找下一个兄弟
continue;
}
// 决策②:type 是否相同?
if (child.elementType === newChild.type) {
// ✅ key 相同 + type 相同 → 复用!
deleteRemainingChildren(returnFiber, child.sibling); // 清兄弟
return useFiber(child, newChild.props); // 复用 DOM,更新 props
}
// ❌ key 相同但 type 不同 → 不能复用,全部删除重建
deleteRemainingChildren(returnFiber, child);
break;
}
// 决策③:没找到可复用的 → 从头创建
return createFiberFromElement(newChild, returnFiber.mode, lanes);
}
核心公式:key 决定”是不是它”,type 决定”能不能用它的骨架”。
2. 四种实际场景
| 场景 | key | type | 结果 | DOM 操作 |
|---|---|---|---|---|
<div /> → <div /> | null=null ✅ | div=div ✅ | 复用 | 0(仅更新属性) |
<div /> → <span /> | null=null ✅ | div≠span ❌ | 重建 | 销毁+创建 |
<div key="a"/> → <div key="b"/> | a≠b ❌ | — | 重建 | 销毁+创建 |
<Comp1/> → <Comp2/> | null=null ✅ | 函数不同 ❌ | 重建 | 销毁+创建 |
3. 为什么 type 不同就必须重建
你无法把一个 <div> DOM 元素变成 <span>——这是浏览器本身的限制。React 不给 DOM 类型做”变形”,直接重建是最正确的选择。
4. deleteRemainingChildren:安全网的清理
即使当前是单节点场景,旧 Fiber 树上可能有多余的兄弟(从多节点变成单节点时):
1
2
3
4
5
6
7
// 之前有 3 个子节点
<div><A/><B/><C/></div>
// 之后变成 1 个
<div><A/></div>
// React 复用 A 后,必须删除 B 和 C
deleteRemainingChildren(returnFiber, child.sibling);
不删的话 B 和 C 的 DOM 会残留在页面上。
5. 无 key 时的行为
没有显式指定 key 时,React 默认为 null。两个无 key 的节点比较时 null === null 总是成立——直接跳过 key 比较阶段。这意味着单节点中不写 key 是安全的,因为没有”和列表里的其他元素对比”的问题。
「其实你每天都在用」
条件渲染
{isLoggedIn ? <Dashboard /> : <Login />}:切换时 type 不同→全量重建。这就是为什么两个组件各自的状态不互通——因为它们在 Fiber 树上根本不是同一个节点。{show && <Modal />}:show 从 false 变 true 时,currentFiber 为 null→全新创建。从 true 变 false 时,旧 Fiber 被标记 Deletion→DOM 销毁。Modal 内部的 state 在关闭时全部丢失。props 更新不重建:
<UserCard id={newId} />,key 相同(默认 null),type 相同(UserCard 函数),走复用路径。组件本身不卸载,只触发 re-render。动画库的”key 重置”技巧:给元素换 key 强制重建 DOM,让 CSS 入场动画重新播放。
React DevTools 里标记的组件:每次切换条件渲染时,你会看到旧组件的 Fiber 被带删除线标记,新组件的 Fiber 在树中出现。
常见误解 (FAQ)
❌ 误区 1:”单节点不需要 key”
技术上不需要。但如果你在两个同 type 的组件之间切换并想强制重建(如清空内部状态),可以手动给不同 key:{mode === 'edit' ? <Form key="edit" /> : <Form key="view" />}——这样 type 相同但 key 不同,React 会销毁+重建。
❌ 误区 2:”条件渲染不写 key 就是安全的”
绝大多数情况是的。但如果你在条件渲染中写 {cond ? <A key="1"/> : <B key="2"/>},两个分支 key 不同,每次切换都是销毁重建——即使 type 相同。除非你需要刻意清空状态,否则别给单节点条件渲染加不同的 key。
❌ 误区 3:”单节点 Diff 就是只比一个”
会比完第一个再沿 sibling 找。如果第一个节点 key 不匹配,React 会继续检查兄弟节点——这在”从多节点变为单节点”的过渡中很重要。
一句话总结
单节点 Diff 的核心原则:先认人(key),再看能不能留(type)——认错人就删,类型不合就重建,全对上就复用——干净利落,一刀切。