React学习-Diff

57 阅读4分钟

Diff 算法

先看一张图,描述了在state\props 更新后,react的工作流程

diff1.png

React 的 Diff 算法(也称为 Reconciliation 算法)是 React 用于高效更新 UI 的核心机制之一。它的目标是在组件状态发生变化时,以最小的 DOM 操作代价将旧的虚拟 DOM(Virtual DOM)树更新为新的虚拟 DOM 树。

Diff 算法的核心就是 - 复用!

1. Diff 的三大核心比较原则

  • 同一层级的节点进行比较(Tree Diff 只在同一层级进行)
  • DOM元素\组件,通过类型判断是否复用
  • 使用 key 属性来判断列表中元素是否复用

2. 比较原则详解

2.1 基本前提:Diff 只在同一层级进行

React 的 Diff 是逐层比较(level-by-level),不会跨层级移动节点。 例如:

// 旧树
<div>
  <span>A</span>
  <p>B</p>
</div>
​
// 新树
<div>
  <p>B</p>
  <span>A</span>
</div>

React 不会认为 <span><p> 是“交换了位置”,而是:

  • 第一个子节点:<span><p>:类型不同 → 销毁 <span>,创建 <p>
  • 第二个子节点:<p><span>:类型不同 → 销毁 <p>,创建 <span>

除非使用 key,否则 React 不会尝试“移动”节点。

2.2 DOM 元素(如 <div> , <span> )的复用判断

判断条件:标签名(type)必须完全相同

  • 如果新旧虚拟 DOM 节点的 type 相同(比如都是 'div'),则复用真实 DOM 节点
  • 然后对比 props,只更新发生变化的属性(如 className, style, onClick 等)。

示例 1:复用 ✅

// 旧 VNode: { type: 'div', props: { className: 'red' } }
// 新 VNode: { type: 'div', props: { className: 'blue' } }

→ 复用同一个 <div> 元素,仅将 className'red' 改为 'blue'

示例 2:不复用 ❌

// 旧 VNode: { type: 'div', ... }
// 新 VNode: { type: 'span', ... }

→ 类型不同,销毁整个旧子树,创建新的 <span> 及其子树。

💡 注意:type 是字符串(原生 DOM 标签)或函数/类(组件)。

2.3 组件(Component)的复用判断

组件分为 函数组件类组件,但 React 对它们的复用判断逻辑一致。

判断条件:组件的构造函数(或函数引用)必须是同一个

也就是说,组件的 type 必须严格相等(===)。

示例 1:复用 ✅(同一组件)

function MyButton({ label }) {
  return <button>{label}</button>;
}
​
// 渲染
<MyButton label="Click" />
// 下次更新
<MyButton label="Press" />

type 都是 MyButton 函数(引用相同)→ 复用组件实例(对函数组件来说是 Fiber 节点),调用新 render(即重新执行函数),但不会卸载/重建。

对于类组件,还会保留 state 和 ref

示例 2:不复用 ❌(不同组件)

{condition ? <LoginButton /> : <LogoutButton />}

LoginButton !== LogoutButton → 即使结构相似,React 也会:

  • 卸载 LoginButton(触发 componentWillUnmount / useEffect cleanup
  • 创建 LogoutButton(触发 constructor / useEffect

示例 3:动态组件陷阱 ❌

function Parent() {
  // ❌ 每次渲染都定义新组件!
  const TempButton = () => <button>Temp</button>;
  return <TempButton />;
}

→ 每次 Parent 重渲染,TempButton 都是一个新的函数引用 → React 认为组件类型变了 → 不断销毁重建 → 状态丢失、性能差。

✅ 正确做法:将组件定义提到外部或用 useMemo 缓存(但通常应避免内联定义组件)。

2.4 列表中节点的复用:key 的作用

当处理兄弟节点列表时(如 map() 渲染),React 默认按索引位置比对。但这样无法正确识别“移动”或“插入”。

key 的核心作用:标识节点的唯一身份

React 通过 key 建立 “旧节点 key → 旧节点” 的映射表,然后遍历新列表:

  1. 对每个新节点,用 key 查找是否有对应的旧节点。
  2. 如果找到且 type 相同 → 复用该 DOM/组件
  3. 如果 type 不同 → 销毁旧节点,创建新节点。
  4. 如果 key 不存在于旧列表 → 创建新节点。
  5. 旧列表中未被匹配的节点 → 删除。

示例:带 key 的列表更新

// 旧
[
  { key: 'a', type: 'li', children: 'Apple' },
  { key: 'b', type: 'li', children: 'Banana' }
]
​
// 新
[
  { key: 'b', type: 'li', children: 'Banana!' },
  { key: 'c', type: 'li', children: 'Cherry' }
]

Diff 过程:

  • key='b':存在,type 相同 → 复用 <li>,更新文本为 'Banana!'
  • key='c':新增 → 创建新 <li>
  • key='a':未被使用 → 删除

✅ 结果:只有 Apple 被删,Banana 节点被复用(保持 focus、动画状态等),Cherry 新增。

如果不用 key(或用 index):

  • 插入到开头会导致后续所有项被重新渲染,即使内容没变。

3. 比较过程

主要分为两个阶段:

第一个阶段:

通过索引一一对比,如果可以复用就下一个,不可以复用就结束。

第二个阶段:

把剩下的旧 fiber 放到 map 里,遍历剩余的 新 vdom,一一查找 map 中是否有可复用的节点。

最后,

把剩下的老 fiber 删掉

剩下的新 vdom 新增