一、Diff 算法的核心设计原则
React 的 Diff 算法并非严格遵循传统 DOM 树的全量对比(时间复杂度 O(n³)),而是通过三大策略将复杂度优化至 O(n):
- 层级比较策略:只对比同一层级的节点,不跨层级移动节点(如删除父节点下的子节点,不会尝试移动到其他层级)。
- 标签类型策略 :若节点标签类型不同,直接删除旧节点并创建新节点(如
<div>
和<span>
视为不同类型,不会复用节点)。 - Key 标识策略 :通过
key
属性识别列表项的唯一性,避免因顺序变化导致的无效更新。
二、Diff 算法的执行流程
Diff 过程分为三个阶段:树层级对比、同级节点对比、节点操作更新。
1. 树层级对比(Tree Diff)
-
规则:新旧虚拟 DOM 树从根节点开始,逐层对比同一层级的节点。
-
案例 :
jsx// 旧树 <div> <Header /> <Content /> </div> // 新树 <div> <Header /> <Sidebar /> <Content /> </div>
- 根节点
<div>
类型相同,继续对比子节点; - 第一层级的
<Header />
相同,保留; - 第二层级旧节点为
<Content />
,新节点为<Sidebar />
和<Content />
,因标签类型不同,删除旧<Content />
,创建新<Sidebar />
,再创建<Content />
。
- 根节点
2. 同级节点对比(Component Diff)
-
规则:若节点为组件,先对比类型,类型相同则继续对比属性,类型不同则卸载旧组件并创建新组件。
-
案例 :
jsx// 旧组件 <Button color="red" /> // 新组件 <Button color="blue" size="large" />
- 组件类型均为
Button
,保留节点; - 对比属性:删除
color="red"
,添加color="blue"
和size="large"
,更新组件状态。
- 组件类型均为
3. 节点操作对比(Element Diff)
- 规则 :针对同一层级的节点列表,通过
key
识别节点身份,分四种情况处理:- 节点新增:新列表存在、旧列表不存在的节点,直接创建;
- 节点删除:旧列表存在、新列表不存在的节点,直接删除;
- 节点更新 :节点类型相同且
key
一致时,对比属性并更新; - 节点移动 :通过
key
识别节点是否需要移动位置(如列表项顺序调整)。
4. 列表 Diff 的关键逻辑(以数组更新为例)
- 场景 :旧列表为
[A, B, C]
,新列表为[B, A, D]
,节点均有唯一key
。 - Diff 过程 :
- 遍历旧列表,用
key
建立映射表({A: 0, B: 1, C: 2}
); - 遍历新列表:
- 新节点
B
:通过key
找到旧列表索引 1,位置不变; - 新节点
A
:找到旧列表索引 0,但当前旧列表已处理到索引 1,需将A
从索引 0 移动到索引 1 之后; - 新节点
D
:旧列表不存在,创建新节点;
- 新节点
- 最终操作:移动
A
到B
之后,删除C
,添加D
。
- 遍历旧列表,用
三、Key 的重要性与反例
-
正确使用 :为列表项提供唯一
key
(如数据库 ID),帮助 Diff 算法精准识别节点。 -
反例(使用索引作为 key) :
jsx// 错误示例:key 为索引 <ul> {list.map((item, index) => <li key={index}>{item}</li>)} </ul> // 当列表前插入新项时,所有后续节点的索引 key 都会变化,导致: // 1. 旧节点被错误删除并重建; // 2. 组件状态(如输入框内容)丢失。
四、Diff 算法的性能优化点
- 跳过无变化的子树 :若组件
props
和状态未改变,直接复用旧节点,不进入子树 Diff; - 批量更新 DOM :通过
requestAnimationFrame
或useEffect
批量执行 DOM 操作,减少重排重绘; - Fiber 架构优化:React 16 后引入 Fiber,将 Diff 过程拆分为可中断的任务,优先处理高优先级更新(如用户交互)。
总结
React 的 Diff 算法通过 分层对比 和 key 标识 策略,在保证性能的同时实现了高效的 DOM 更新。理解其原理有助于优化组件设计(如正确使用 key)、定位性能瓶颈(如频繁的节点重建),并为复杂列表操作(如拖拽排序)提供优化思路。