跳到主要内容

简单介绍一下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:

  • keytype 都能对上,就复用
  • 一旦连续顺序对不上,再进入更复杂处理

这种设计是因为很多真实更新本来就是“末尾追加”或“小范围改动”。

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 节点打上 PlacementUpdateDeletion 等标记

所以 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
  • 目标是工程上足够快,不是理论最优。