简单介绍一下React中的diff算法
面试速答(30 秒版 TL;DR)
- React 不做理论最优树编辑算法,因为那样复杂度太高,实际工程里不划算。
- React 采用的是 启发式 diff,核心目标是:在足够快的时间里,得到足够好的更新结果。
- 最重要的三条规则:
- 只做同层比较
- 节点类型不同,通常直接替换整棵子树
- 列表更新主要依赖
key
- 在 Fiber 架构下,diff 不是孤立算法,而是嵌在 Reconciler 的子节点协调流程里。
- 一句话总结:React diff 追求的是 O(n) 级别的高频场景可用解,而不是全局最小编辑次数。
一、为什么 React 不能用“最优 diff”
如果严格求两棵树的最小编辑步骤,复杂度会非常高。
UI 框架面对的是高频更新场景:
- 输入联想
- 列表筛选
- 局部状态变化
- 页面切换
React 更看重的是:
- 常见场景下足够快
- 规则简单稳定
- 能和组件模型、调度模型配合
所以它采用启发式策略,把复杂度压到更可接受的范围。
二、React diff 的三条核心假设
1. 只做同层比较
React 默认不会跨层级去找“是不是同一个节点”。
例如一个节点原来在父组件 A 下面,后来跑到另一个层级,React 通常不会把它当成“移动”,而会更倾向于:
- 旧节点卸载
- 新节点重新挂载
这条规则的意义是大幅降低比较复杂度。
2. 类型不同,通常直接替换
比如旧节点是:
<div className="box" />
新节点是:
<span className="box" />
即使属性很像,只要宿主类型变了,React 一般也会认为不是同一个节点,直接重建。
对于组件来说也类似:
OldComponent变成NewComponent- 通常就按不同子树处理
3. 列表复用主要看 key
列表是 diff 最常见的复杂场景。
React 会优先拿 key 判断:
- 哪个旧节点能复用
- 哪个节点要新建
- 哪个节点应该删除
- 哪个节点只是位置变了
如果 key 不稳定,比如用数组索引,在头部插入元素时就容易出现:
- 状态错位
- 不必要重建
- 额外 DOM 操作
三、React 在列表上大致怎么 diff
你不用背源码,但要知道大致流程。
以同一层的一组 children 为例,React 大致会这样做:
1. 先尝试顺序复用
从左到右对比新旧 children:
key和type都能对上,就复用- 一旦连续顺序对不上,再进入更复杂处理
这种设计是因为很多真实更新本来就是“末尾追加”或“小范围改动”。
2. 为剩余旧节点建立映射
当顺序复用失败后,React 会把剩余旧节点按:
key- 或无
key时按索引
组织成映射结构,方便后续快速查找。
3. 遍历新 children 找可复用节点
遍历新的 children:
- 找到同
key且同类型的旧节点,则复用 - 找不到,就创建新 Fiber
同时 React 会判断这个节点是:
- 原地复用
- 需要移动
- 全新插入
4. 未被消费的旧节点统一删除
如果某些旧节点最终没有在新列表里找到匹配项,React 会把它们标记为删除。
四、为什么说 React diff 是“启发式”而不是“精确最优”
因为 React 并不承诺:
- 全局最少 DOM 操作
- 全局最优移动方案
它承诺的是:
- 大多数 UI 更新场景表现足够好
- 复杂度可控
- 可以和 Fiber 调度结合
这也是面试里非常值得强调的一点:
React diff 的目标从来不是数学上最优,而是工程上高性价比。
五、在 Fiber 时代,diff 发生在哪里
很多人把 diff 理解成一个独立“大函数”,这不够准确。
在 Fiber 架构下,diff 发生在 Reconciler 的工作循环中,尤其是:
- 当前 Fiber 重新 render 出新 children
- React 用子节点协调逻辑把新 children 和旧 child Fiber 链表对比
- 最终为每个 Fiber 节点打上
Placement、Update、Deletion等标记
所以 React diff 最终产出的是:
- 哪些 Fiber 复用
- 哪些 Fiber 新建
- 哪些 Fiber 删除
- 提交阶段要执行哪些副作用
六、面试时怎么讲得又短又稳
标准答法
React 的 diff 是一种启发式的同层比较算法,不追求理论上的最小编辑次数,而是追求工程上足够快。它基于三个核心规则:同层比较、节点类型不同直接替换、列表更新依赖 key。列表 diff 时会先尝试顺序复用,乱序后再为剩余旧节点建映射,用 key 找可复用节点,并标记插入、移动和删除。Fiber 架构下,diff 结果最终会体现在各个 Fiber 节点的副作用标记上,再由 commit 阶段统一提交。
七、常见追问
1. 为什么不推荐用索引当 key?
因为列表插入、删除、重排时,索引会变化,React 会误判节点身份,导致复用错位。
2. React 会不会跨层级移动节点?
默认不会按“跨层搬家”做复杂匹配,通常按卸载旧节点、挂载新节点处理。
3. React diff 是 O(n) 吗?
常见启发式路径下可以近似理解成 O(n) 级别,但别把它答成“所有场景绝对严格 O(n)”。
八、易错点 / 坑
- 把 React diff 说成“深度优先暴力比较所有节点”。
- 把
key说成“只是为了消除警告”。 - 说 React 一定会求最小 DOM 操作数。
- 把 Vue 3 的 LIS 优化细节直接套到 React 上。
速记要点(可背诵)
- React diff = 启发式同层比较。
- 类型不同,通常直接替换。
- 列表复用主要依赖
key。 - 目标是工程上足够快,不是理论最优。