Diff 算法
先看一张图,描述了在state\props 更新后,react的工作流程
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 → 旧节点” 的映射表,然后遍历新列表:
- 对每个新节点,用
key查找是否有对应的旧节点。 - 如果找到且
type相同 → 复用该 DOM/组件。 - 如果
type不同 → 销毁旧节点,创建新节点。 - 如果
key不存在于旧列表 → 创建新节点。 - 旧列表中未被匹配的节点 → 删除。
示例:带 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 新增