创见博客
React Diff 算法
七崽爱吃小饼干2026/01/04阅读 1专栏 React

React Diff 算法的三大核心比较(层级/节点/列表)详解

React Diff 算法的对比过程并非单一维度,而是分层次、分场景的三级比较策略,分别是层级比较(树级对比)、节点类型比较(元素级对比)、列表节点比较(子节点列表对比),这三级比较由顶层到底层依次执行,共同实现高效的虚拟 DOM 差异检测。

一、 第一级:层级比较(树级对比)—— 自上而下的分层比对

层级比较是 React Diff 算法的顶层策略,也是整个 Diff 算法的性能基石,我们在之前的对话中已初步提及,这里进行更详细的拆解:

1. 核心逻辑

React 会将虚拟 DOM 树视为一棵“分层嵌套”的树形结构,对比新旧虚拟 DOM 树时,只自上而下地比对同一嵌套层级的节点,不进行跨层级的节点匹配和对比。

简单来说:React 从根节点开始,先对比根节点(层级 0),再分别对比根节点的子节点(层级 1),接着对比每个层级 1 节点的子节点(层级 2),以此类推,每层节点只和它“正下方”同一层级的节点比对,绝不会跳过当前层级去比对深层级节点,也不会把深层级节点和上层级节点交叉比对。

2. 对比行为与结果

  • 对于每个层级的节点,React 会先判断该层级是否存在节点(新增/删除),再对该层级内的节点进行后续的类型比较;
  • 若发现某一节点在新虚拟 DOM 树中“跨层级移动”(如层级 2 的节点移到层级 1),React 不会识别为“移动操作”,而是判定为:旧层级中该节点被删除,新层级中该节点是新增节点,最终执行“删除旧节点 + 创建新节点”的操作;
  • 若某一层级的节点结构完全不变,React 会直接递归进入该节点的子层级,继续进行层级比对。

3. 设计目的

放弃跨层级对比,将树形 Diff 的时间复杂度从传统树形 Diff 算法的 O(n³)(n 为节点数量)降低到**接近 O(n)**的线性复杂度,极大减少无效的遍历和计算,保证在绝大多数场景下的性能最优(因为实际开发中,跨层级移动 DOM 节点是极少出现的边缘场景)。

4. 直观示例

// 旧虚拟 DOM 树
层级 0:<div>(根节点)
        ↓
层级 1:<header>、<main>
        ↓        ↓
层级 2:<h1>    <p>、<ul>

// 新虚拟 DOM 树(<h1> 从层级 2 移到层级 1)
层级 0:<div>(根节点)
        ↓
层级 1:<h1>、<header>、<main>
        ↓                ↓
层级 2:                 <p>、<ul>

React 的层级对比行为:

  1. 对比层级 0:<div> ↔ <div>,无差异,递归进入层级 1;
  2. 对比层级 1:旧节点 <header>、<main> 与新节点 <h1>、<header>、<main> 比对,认为 <h1> 是新增节点;
  3. 对比层级 2:旧节点 <h1> 与新节点(无 <h1>)比对,认为 <h1> 被删除;
  4. 最终执行:删除层级 2 的 <h1> + 新增层级 1 的 <h1>,而非识别为 <h1> 跨层级移动。

二、 第二级:节点类型比较(元素级对比)—— 单个节点的快速判断

当完成层级对比,确定需要对某一层级内的单个节点进行比对时,React 会执行节点类型比较,这是一种“快速预判”策略,用于快速决定节点的处理方式(复用更新 / 销毁重建)。

1. 核心逻辑

React 会对比新旧虚拟 DOM 节点的「类型」,不同类型的节点对应完全不同的处理逻辑,无需深入节点内部进行复杂比对。

2. 两种核心场景(类型相同 vs 类型不同)

场景 1:新旧节点类型相同

若新旧节点的类型一致(如均为 <div>、均为同一个自定义组件 <TodoItem>、均为文本节点等),React 会判定该节点可以复用,并执行以下操作:

  1. 保留该节点的真实 DOM 实例,不进行销毁和重建;
  2. 对比新旧节点的 props 属性(如 className、style、onClick 等),找出属性的差异(如 className 从 a 变为 b、style 颜色变化等);
  3. 仅将属性差异批量更新到真实 DOM 上,然后递归进入该节点的子节点,继续执行后续的对比逻辑。

示例:

jsx
// 旧节点
<div className="old" style={{ color: 'black' }}>Hello</div>

// 新节点(类型相同,props 有差异)
<div className="new" style={{ color: 'red' }}>Hello</div>

React 处理:复用 <div> 真实 DOM,仅更新 className 和 style 属性,不重新创建 <div> 节点。

场景 2:新旧节点类型不同

若新旧节点的类型不一致(如 <div> 变为 <p>、<h1> 变为自定义组件 <Button>、文本节点变为 <span> 等),React 会直接判定该节点无法复用,并执行以下“暴力操作”:

  1. 立即销毁旧节点对应的真实 DOM(包括该节点的所有子节点,递归销毁);
  2. 根据新节点的类型,创建对应的新真实 DOM 节点;
  3. 无需深入旧节点内部比对子节点(因为整个节点树都被销毁),直接将新节点挂载到真实 DOM 中。

示例:

jsx
// 旧节点
<h1 className="title">旧标题</h1>

// 新节点(类型不同)
<p className="title">新文本</p>

React 处理:销毁旧的 <h1> 真实 DOM 及其所有子节点,创建新的 <p> 真实 DOM 并挂载。

3. 特殊补充:特殊节点类型

对于 <svg>、<input> 等特殊标签节点,React 会在类型比较时额外判断标签的具体类型(如 <input type="text"> 与 <input type="checkbox"> 会被视为类型不同的节点),避免错误复用。

三、 第三级:列表节点比较(子节点列表对比)—— 针对列表的精细化比对

当节点的子节点是“列表形式”(多个同级子节点组成的数组,如 map 渲染的列表)时,React 会执行列表节点比较,这是 Diff 算法中最复杂、最精细的一层对比,核心解决列表增删、排序、移动的差异检测问题。

1. 核心背景

如果直接按索引顺序比对列表节点,在列表发生“非尾部增删”或“排序”时,会导致大量节点被错误地销毁和重建(而非复用),造成严重的性能损耗和状态混乱,因此 React 专门设计了列表节点的对比逻辑。

2. 核心逻辑

列表节点比较的核心是基于 key 的唯一标识匹配,而非按索引顺序匹配,整体分为两个步骤:

步骤 1:通过 key 建立新旧列表节点的映射关系

React 会先遍历新列表或旧列表,以 key 为唯一标识,建立新旧节点的对应关系,快速识别出:

  • 可复用节点:新旧列表中存在相同 key 的节点(判定为同一个节点,可复用);
  • 新增节点:新列表中有、旧列表中没有的 key 对应的节点;
  • 删除节点:旧列表中有、新列表中没有的 key 对应的节点。
步骤 2:检测节点移动并优化更新

对于可复用的节点,React 会进一步对比它们在新旧列表中的位置,判断是否发生了移动操作:

  • 若节点位置未变化:仅更新节点的 props 差异(同节点类型比较的逻辑);
  • 若节点位置发生变化:不销毁重建节点,仅通过 DOM 操作调整节点的位置(如 insertBefore),这是极大的性能优化。

3. 两种对比策略(简单场景 vs 复杂场景)

策略 1:简单列表(无排序/移动,仅尾部增删)

对于仅在尾部进行增删的列表,React 会采用“按索引顺序比对”的简化策略,此时即使使用索引作为 key,也不会出现问题,因为节点的 key 与节点的对应关系始终稳定。

示例:

jsx
// 旧列表(尾部增删)
<ul>
  <li key={0}>项1</li>
  <li key={1}>项2</li>
</ul>

// 新列表(尾部新增项3)
<ul>
  <li key={0}>项1</li>
  <li key={1}>项2</li>
  <li key={2}>项3</li>
</ul>

React 处理:按索引比对,复用项1、项2,新增项3,逻辑简单高效。

策略 2:复杂列表(有排序/移动/非尾部增删)

对于存在排序、移动或非尾部增删的列表,React 会启用“基于 key 的映射比对 + 移动检测”策略,避免节点错误复用。

示例(列表排序):

jsx
// 旧列表
<ul>
  <li key="todo1">学习 React</li>
  <li key="todo2">理解 Diff 算法</li>
</ul>

// 新列表(排序交换)
<ul>
  <li key="todo2">理解 Diff 算法</li>
  <li key="todo1">学习 React</li>
</ul>

React 处理:

  1. 通过 key 识别出 todo1 和 todo2 都是可复用节点;
  2. 检测到它们的位置发生了交换;
  3. 不销毁重建 <li> 节点,仅调整两个 <li> 在真实 DOM 中的位置,性能最优。

4. 无 key 或 key 不当的危害

如之前对话所述,无 key 或使用索引/随机数作为 key,会导致 React 无法建立正确的节点映射关系,出现:

  • 节点无法复用,大量销毁重建(性能损耗);
  • 节点错误复用,导致组件状态(如表单值)混乱;
  • 无法识别节点移动,只能暴力重建列表。

总结

React Diff 算法的三大核心比较是自上而下、层层递进的,核心要点如下:

  1. 层级比较(树级):只比对同一嵌套层级的节点,不跨层级,是性能基石,时间复杂度接近 O(n);
  2. 节点类型比较(元素级):快速预判节点是否可复用,类型相同则更新 props,类型不同则销毁重建;
  3. 列表节点比较(子节点列表级):基于 key 进行唯一标识匹配,支持节点复用和移动检测,解决列表增删排序的优化问题;
  4. 三者关系:层级比较决定“对比哪些层级”,节点类型比较决定“单个节点是否复用”,列表节点比较决定“列表子节点如何精细化复用和调整”。
评论
0/100