react 的 diff 原理
React 的 diff(调和 reconciliation)原理是 React 高性能的核心关键,它决定了当 state / props 发生变化时,React 如何高效更新 DOM。
React 为什么需要 Diff?
原生 DOM 操作非常慢,如果每次状态变化就 重新生成整个 DOM,性能会非常差。
React 通过 虚拟 DOM(vDOM) 和 diff 算法:
-
找出“变化的部分”
-
只更新必要的 DOM 节点(最小代价)
-
避免整棵 DOM 重建
React Diff 的三个核心策略(非常重要)
React 的 diff 不是通用树 diff(O(n³)),而是基于实际 UI 情况进行了优化,将复杂度降低到 O(n)。
策略 1:不同类型的节点,直接销毁重建
例如:
<div>...</div>
<span>...</span>
React 不会比较子节点,直接删除旧节点,创建新节点。
策略 2:同类型节点,进行“细粒度比较”
例如:两个 <div>:
-
React 会比较它们的 props
-
只更新变化的属性,如 className、style 等
-
再递归比较子节点
策略 3:对子节点列表(数组)使用 key,提高 diff 性能
列表的核心是 key 决定元素是否是同一个节点。
例如,没有 key:
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
如果你把 B 移到开头:
B A C
没有 key 的话,React 可能会:
-
认为 A→B、B→A
-
产生大量不必要的 DOM 移动
但如果 key 明确:
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
React 会:
-
根据 key 判断哪些节点可以复用
-
只移动必要的节点
-
性能非常高
React diff 的 3 种情况(详细流程)
React Diff 分为三种情况处理 vDOM 树:
情况 1:节点类型变化
旧 vDOM:<div />
新 vDOM:<span />
操作:
-
旧 DOM 节点全部卸载
-
新 DOM 节点重新创建
情况 2:节点类型相同
旧 vDOM:<div class="a">...
新 vDOM:<div class="b">...
操作:
-
比较 props → 更新变化属性
-
继续 diff 子节点
情况 3:列表 diff(重点)
React 使用 “从左到右、逐个对比 + key 搜索” 的策略。
核心步骤:
-
旧节点按 key 建 hashMap(O(n))
-
新列表逐个查 key(O(1) 查找)
-
判断:
-
是否能复用旧节点?
-
是否需要移动位置?
-
是否需要插入新节点?
-
是否要删除旧节点?
key 的作用:
-
key 不变 → 认为是同一个节点,更新内容,不重建
-
key 变化 → 认为是不同节点
错误:使用 index 作为 key
会导致:
-
节点复用错误
-
动画错乱
-
state 绑定错误(比如 input 的选中状态乱掉)
-
性能下降,因为大量 DOM 需要替换
React Fiber 与 Diff 的关系
React 16+ 的 diff 基于 Fiber 架构,但核心思想不变:
Fiber 将更新拆成更小的“任务单元”
支持中断、恢复(可响应优先级,比如界面不卡顿)
每个 Fiber 节点都存储:
-
当前 vDOM
-
更新后的 vDOM
-
DOM 操作标记(比如 Placement, Update, Deletion)
Diff 发生在 Fiber 的 beginWork 与 completeWork 阶段。
总结
React Diff 原理
React 使用的是 O(n) 的启发式 tree diff 算法:
-
不同类型节点 → 直接重建
-
同类型节点 → 比较 props + 继续 diff 子节点
-
列表使用 key → 快速定位可复用节点,减少 DOM 操作
面试时的总结
-
React 不是通用 diff,时间复杂度 O(n³) → O(n)
-
核心优化点:分层比较、类型判断、key 定位
-
key 是 diff 的灵魂,使用 index 会产生问题
-
Fiber 将 diff 任务进一步切分,带来可中断渲染
浙公网安备 33010602011771号