Day 5 核心内容:Diff 算法

0 阅读6分钟

React 源码面试冲刺 - Day 5

日期:2026-03-19 主题:Diff 算法


📖 Day 5 核心内容:Diff 算法

👴 老大爷能听懂版

Diff = React 的"火眼金睛",找出哪里不一样

场景操作
你改了按钮文字只更新这个按钮
你删了一个列表项只删除那个
你加了新内容只加新的那一块

Diff 的核心思想:不要全量更新!

传统虚拟DOM:整个树对比 → 慢!
React Diff:只对比变化的部分 → 快!

Diff 两大原则:

  1. 同级比较(只看同一层,不跨级)

    父            父
    ├─ 子1   →   ├─ 子1  ✅ 同级
    ├─ 孙1       ├─ 孙1  ✅ 同级
    ✗ 不比较兄弟的孙子
    
  2. 不同类型 = 重建

    <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+:

  • ❌ 调度器原理
  • ❌ 并发模式
  • ❌ 状态管理原理

💪 今日自测

  1. React Diff 的两个核心原则是什么?
  2. 为什么不能用 index 作为 key?
  3. type 不同的节点怎么处理?
  4. 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 怎么"排队"干活的 ⏰