React diff 机制

22 阅读4分钟

🧠 React diff

React 框架的核心目标是在最小 DOM 更新成本下,让 UI 和最新状态保持一致。 由于 DOM 操作非常昂贵,如果每次更新都全量重绘,那性能会炸掉。 所以 React 在虚拟 DOM(vDOM)层做对比:新旧两棵树做差异计算(diff) → 最小代价更新真实 DOM

但树的最优 diff 是 O(n³) ,不可接受。
React 把这个问题降为 O(n) —— 用一套启发式策略。

这就是“diff 机制”的来源。

在 Fiber 之前是 深度优先遍历(DFS)。 Fiber 之后依旧是深度优先,只是基于链表结构执行。

🧩 React Diff 的三大核心策略

策略 1:不同类型组件,直接卸载并重建(type 变就全重建)

<div /> → <span />

当组件的 type 改变后,React 不会继续向下深比较,而是:

1️⃣ 卸载旧节点(包括生命周期 componentWillUnmount)

2️⃣ 创建新节点

3️⃣ 渲染

这么做的原因可以总结为以下几点:

  • 不同类型的组件结构差异大,深比较成本不值得
  • 类型是一种非常稳定的区分方式
  • 提升 diff 的确定性和速度

React 不会跨类型 diff

策略 2:同一类型节点只比较 props & children

<div className="a"></div> → <div className="b"></div>

当节点类型相同时,React 会按照以下步骤处理:

1️⃣ keep 节点不动

2️⃣ 对比属性差异

3️⃣ 再看 children

这是 vDOM diff 的核心加速点:同类型节点 → 只做局部 diff,不重建结构。

策略 3:列表 diff 必须依赖 key 来定位元素

[ A, B, C ][ B, C, A ]

如果 key 写得好(如数据库 ID),React 会聪明地:

  • B → 保留
  • C → 保留
  • A → 移到最后
  • 不会重建组件,不会丢 state

但如果你写 key={index}

React 只能按索引位置猜测重用,会导致:

  • 组件状态错位
  • 多余重渲染
  • 动画/输入丢失

key 代表节点的身份,必须是一个稳定的值,而 index 代表的是在列表中的位置,不是身份。当顺序变动时,会导致错误 diff、错误重用

🧵 Diff 工作流程

🔹 1. 节点层(Element Diff)

对比规则:

  1. type 相同 → 复用 DOM/组件实例 → 比较 props
  2. type 不同 → 直接替换
  3. 对比 ref 是否变化
  4. 最后去 diff children

流程非常稳定且确定。

🔹 2. 子节点层(Children Diff)

React 针对 children 类型不同,采用不同策略:

文本节点

直接比较字符串是否相同,不同则替换 textContent。

② 单节点

直接按第一条策略走:type 相同则深入 diff。

③ 多节点(列表 diff)

React 会:

  1. 第一遍扫描:确定 “老节点哪些能复用”
  2. 第二步:解决“移动”和“新增”
  3. 最后:删除不再存在的节点

React 做了优化:

  • 按 key 建 Map,加速定位
  • 头尾双端比较(快速处理常见场景:新增、删除、尾插)

假设有一个列表 ABC,经过更新后变成 BCD,React 处理的步骤如下:

操作说明
删除A 不在新列表中,会被销毁
移动BC 的 key 相同,但位置变化,React 会移动 DOM 节点
添加D 是新节点,会创建新的 DOM

💡 3. 头尾双端比较

当列表节点数量比较大时,React 内部优化了 diff 算法,尤其是 移动、插入、删除的场景。核心是:

  1. 同时从头和尾进行比较

    • 新旧列表各有两个指针:oldStartoldEndnewStartnewEnd

    • 步骤:

      1. 比较 oldStartnewStart
      2. 比较 oldEndnewEnd
      3. 比较 oldStartnewEnd(头移尾)
      4. 比较 oldEndnewStart(尾移头)
    • 一旦匹配成功,就移动指针继续向内推进。

  2. 核心思路

    • 前后双向扫描:快速找到头尾未移动的节点,减少中间查找次数

    • 处理剩余未匹配节点

      • 新节点未匹配 → 创建
      • 老节点未匹配 → 删除
      • 中间节点可能移动 → 使用 key 映射表快速定位
  3. 时间复杂度优化

    • 传统 diff 是 O(n²)(每个新节点找老节点)
    • 双端 + key 映射 → O(n),这是 Vue 3 和 React Fiber 中 diff 的优化思路。

举个简单例子

旧列表:[A, B, C, D]
新列表:[B, A, E, D]

  • 步骤:

    1. oldStart=A, newStart=B → 不匹配 → 尝试 oldEnd=D, newEnd=D → 匹配,D 不动

    2. oldStart=A, newStart=B, oldEnd=C, newEnd=E → 头尾扫描都不匹配

    3. 建立 key 索引表:{A:0, B:1, C:2, D:3}

    4. 扫描新列表:

      • B → 在老列表 index=1 → 移动到新位置
      • A → index=0 → 移动
      • E → 不在老列表 → 创建
  • 剩余 C → 不在新列表 → 删除

最终操作:移动 BA,删除 C,创建 E

⭐ 4. 节点处理

React 通过 key 来判断节点的“身份”,并且记录其旧位置和新位置。如果一个节点的新位置比之前小,就会标记为“需要移动”。不过 React 不会真的立刻移动 DOM,它只是:

  • 记一个 “Placement” 副作用 tag
  • 等到 commit 阶段一次性操作 DOM

🚀 总结

  • “React diff 是 O(n) 的启发式算法,不追求最优解,而追求高效可预测。”
  • “核心是 type 比较 + children diff + key 定位。”
  • “diff 和 Fiber 紧密结合,前者负责找差异,后者提供调度能力。”
  • “key 决定节点身份,是正确复用的根基。”