React 源码面试冲刺 - Day 5
日期:2026-03-19 主题:Diff 算法
📖 Day 5 核心内容:Diff 算法
👴 老大爷能听懂版
Diff = React 的"火眼金睛",找出哪里不一样
| 场景 | 操作 |
|---|---|
| 你改了按钮文字 | 只更新这个按钮 |
| 你删了一个列表项 | 只删除那个 |
| 你加了新内容 | 只加新的那一块 |
Diff 的核心思想:不要全量更新!
传统虚拟DOM:整个树对比 → 慢!
React Diff:只对比变化的部分 → 快!
Diff 两大原则:
-
同级比较(只看同一层,不跨级)
父 父 ├─ 子1 → ├─ 子1 ✅ 同级 ├─ 孙1 ├─ 孙1 ✅ 同级 ✗ 不比较兄弟的孙子 -
不同类型 = 重建
<div> → <span> ❌ 完全不同,删掉 div 重建 span <div> → <div> ✅ 同类型,复用
💻 专业开发者版
Diff 算法的三个策略:
// React 的 reconcile 策略
function reconcileChildFibers(returnFiber, newChildren) {
// 策略1:同级比较(React 16+)
// 只比较同一层级的节点,不跨层级
// 策略2:key 匹配
// 有 key 用 key 匹配,没有才用索引
// 策略3:类型判断
// type 不同 → 直接删除重建
// type 相同 → 属性复用
}
Diff 流程图:
新 children 进来
↓
遍历新 children
↓
┌──────┴──────┐
↓ ↓
有 key 没有 key
↓ ↓
key 匹配 索引匹配
↓ ↓
复用/移动 可能有 bug
具体 Diff 逻辑:
// 简化版 Diff 算法
function diff(oldFiber, newElement) {
// 1. type 不同 → 删掉重建
if (oldFiber.type !== newElement.type) {
return createFiber(newElement); // 旧的不要了
}
// 2. type 相同 → 复用 + 对比 props
return {
...oldFiber,
pendingProps: newElement.props, // 更新属性
flags: Update, // 标记需要更新
};
}
key 的作用(最重要!):
// 没有 key(用索引)
['a', 'b', 'c'] → ['a', 'b', 'c', 'd'] // 追加
// React 认为:第0个还是a,第1个还是b...
['a', 'b', 'c'] → ['c', 'a', 'b'] // 顺序变了
// React 认为是:第0个从a变成c,第1个从b变成a...
// 💥 大规模 DOM 重建!
// 有 key(用 key 匹配)
[{id:1, v:'a'}, {id:2, v:'b'}] → [{id:1, v:'a'}, {id:2, v:'b'}, {id:3, v:'c'}]
// React 认识 id=1, id=2,只是新增 id=3
// ✅ 完美复用!
四种 Diff 场景:
| 场景 | 操作 | 复杂度 |
|---|---|---|
| 新增 | 添加新节点 | O(n) |
| 删除 | 移除旧节点 | O(n) |
| 更新 | 属性/内容变化 | O(n) |
| 移动 | 位置变化 | O(n) 最难 |
移动算法(React 的优化):
// React 用"最长递增子序列"优化移动
// 找到不需要移动的元素,其他移动
// 例子:[A, B, C, D] → [D, A, B, C]
//
// React 分析:D 要移到最前面
// 剩余 A, B, C 顺序没变 → 不需要移动
// 只需要把 D 移到开头
💪 面试高频问题
| 问题 | 答案要点 |
|---|---|
| React Diff 原理? | 同级比较,只看同一层,不跨层级 |
| key 的作用? | 精确匹配节点,复用 DOM,避免错乱 |
| 为什么不能用 index 做 key? | 顺序变化时会导致 DOM 错乱、状态丢失 |
| Diff 的复杂度? | O(n),通过同级比较优化 |
| type 不同的组件会怎样? | 直接删除重建,不会复用 |
| 如何优化 List 性能? | 用稳定唯一的 key,远�� index |
📊 Day 1-5 面试能力评估
Day 1-5 学完能答:
- ✅ React 核心 API
- ✅ JSX 转换原理
- ✅ Hooks 原理
- ✅ Fiber 架构
- ✅ Diff 算法 ← 今日新增
还需要 Day 6+:
- ❌ 调度器原理
- ❌ 并发模式
- ❌ 状态管理原理
💪 今日自测
- React Diff 的两个核心原则是什么?
- 为什么不能用 index 作为 key?
- type 不同的节点怎么处理?
- key 怎么帮助 React 优化性能?
📝 详细答案
1. React Diff 的两个核心原则是什么?
原则一:同级比较(Same Level)
父
├─ A ← 只和同级比较
├─ B ← 不会和 A 的子类比较
└─ C
React 只比较同一层级的节点,不比较跨层级的节点。
举例:
<div>
<A />
</div>
↓ 变成
<span>
<A /> ← A 被删除了!因为父节点类型变了
</span>
原则二:类型不同 = 重建(Different Type = Recreate)
// type 不同,直接删除重建
<div> → <span> // 删掉 div,新建 span
<p> → <div> // 删掉 p,新建 div
2. 为什么不能用 index 作为 key?
核心问题:index 不稳定,顺序变了 key 就变了
// 初始:['a', 'b', 'c']
// key 用 index:['a', 'b', 'c']
// index:0 1 2
// 删除第一个后:['b', 'c']
// key 用 index:['b', 'c']
// index:0 1 ← 💥 全变了!
// React 看到的:
// - index 0 从'a'变成'b' → 认为 a 变了,更新 DOM
// - index 1 从'b'变成'c' → 认为 b 变了,更新 DOM
// - index 2 不存在了 → 删除!
// 结果:整个列表全部重新渲染!
对比正确的 key:
// 初始:[{id:1, v:'a'}, {id:2, v:'b'}, {id:3, v:'c'}]
// key 用 id:[{id:1, v:'a'}, {id:2, v:'b'}, {id:3, v:'c'}]
// 删除第一个后:[{id:2, v:'b'}, {id:3, v:'c'}]
// key 用 id:[{id:2, v:'b'}, {id:3, v:'c'}]
// React 看到的:
// - id 2 还在,只是位置变了 → 调整 DOM 位置
// - id 3 还在,只是位置变了 → 调整 DOM 位置
// - id 1 不见了 → 删除
// 结果:只删除了第一个,其他复用!✅
常见错误场景:
// ❌ 错误:表格排序、删除、插入
items.map((item, index) => <li key={index}>{item.name}</li>)
// ✅ 正确:用唯一 ID
items.map(item => <li key={item.id}>{item.name}</li>)
3. type 不同的节点怎么处理?
答案:直接删除重建,不复用!
// 例子
<div className="old">Hello</div>
↓ 更新后
<span className="new">Hello</span>
// React 分析:
// - type: 'div' → 'span' 不同!
// - flags: Placement + Deletion + Creation
// - 结果:删除 div,新建 span
为什么这样做?
- type 不同,内部结构完全不同
- 强行复用会导致 DOM 结构错误
- 重建虽然慢,但保证正确性
特殊情况:
// 同类型但不同标签?
<div /> → <p /> // ❌ 不同 type,重建
// 同类型?
<div /> → <div className="new" /> // ✅ 复用,只更新 props
4. key 怎么帮助 React 优化性能?
核心:key 让 React "认识"每个节点
没有 key:
list.map((item, i) => <li key={i}>)
有 key:
list.map(item => <li key={item.id}>)
key 的作用:
| 无 key(有隐患) | 有 key(正确) |
|---|---|
| 用索引匹配 | 用唯一ID匹配 |
| 顺序变化→错乱 | 顺序变化→只移动 |
| 状态可能丢失 | 状态保持 |
| 性能差 | 性能好 |
实际例子:
// 场景:用户列表,删除第二个
// ❌ 没有 key → 灾难!
['用户A', '用户B', '用户C']
↓ 删除 index 1
['用户A', '用户C']
React 以为:
- index 0: '用户A' → '用户A' 不变
- index 1: '用户B' → '用户C' 变了!更新 DOM
- index 2: '用户C' → 没了,删除 DOM
结果:DOM 操作3次,其中1次是错的!
// ✅ 有 key → 完美!
[{id:1, name:'用户A'}, {id:2, name:'用户B'}, {id:3, name:'用户C'}]
↓ 删除 id=2
[{id:1, name:'用户A'}, {id:3, name:'用户C'}]
React 认识:
- id=1 还在
- id=2 没了 → 删除
- id=3 还在
结果:DOM 操作1次(只删除)✅
最佳实践:
// 1. 用数据库 ID
items.map(item => <li key={item.id}>)
// 2. 用 UUID(不推荐每次生成,在数据层生成)
items.map(item => <li key={item.uuid}>)
// 3. 真的没有 ID → 用稳定的组合 key
items.map((item, i) => <li key={`${item.type}-${i}`}>)
// 4. 万不得已才用 index,但要确保列表不变
// 只有纯展示列表,不增删改才可以用
items.map((item, i) => <li key={i}>)
Day 5 ✅ 完成!Diff 算法核心:同级比较 + key 匹配 + type 判断 = O(n) 性能!
明天 Day 6,继续抠调度器原理,看看 React 怎么"排队"干活的 ⏰