React:diff 算法
React 的 diff 算法是一种用于高效更新 DOM 的机制,它通过对比新旧虚拟 DOM 树的差异,只更新需要变化的部分。以下是其核心原理的简化说明:
React 维护虚拟 DOM 树,数据变化时生成新树。通过 Diff 算法对比新旧树,找出差异部分(Patch),将其加入队列后批量更新到真实 DOM。
1. 虚拟 DOM (Virtual DOM)
React 使用 JavaScript 对象表示 UI 结构(虚拟 DOM)。当状态变化时,React 会生成新的虚拟 DOM 树。
2. Diffing 算法的目标
比较新旧两棵树,找出最小的 DOM 操作集合。为了优化性能,React 采用了以下策略:
3. 核心策略
3.1 树比较(tree diff)
-
层级比较:React 只会比较同层级的节点,不会跨层级移动节点。如果发现某层级的节点被移除,React 会直接销毁该节点及其所有子节点。
jsx// 旧树 <div> <ChildA /> // 被移除 <ChildB /> // 保持不变 </div> // 新树 <div> <ChildB /> // 保持不变 <ChildC /> // 新增 </div>
React 会:
- 销毁
ChildA
- 保留
ChildB
(假设组件类型相同) - 创建
ChildC
- 销毁
3.2 组件比较(component diff)
-
相同类型组件 :React 会保留组件实例,只更新其 props 和状态,然后调用
render()
方法生成新的虚拟 DOM。 -
不同类型组件:React 会销毁旧组件,创建新组件
jsx// 旧组件 <Header /> // 类型为Header // 新组件 <Sidebar /> // 类型为Sidebar
React 会:
- 调用
Header
的componentWillUnmount()
- 创建
Sidebar
实例并调用componentDidMount()
- 调用
3.3 元素比较(element diff)
-
相同类型的 DOM 元素:React 会保留 DOM 节点,只更新变化的属性。
jsx// 旧元素 <div className="old" /> // 新元素 <div className="new" />
React 会:
- 保留
<div>
节点 - 更新
className
属性
- 保留
-
不同类型的 DOM 元素:React 会销毁旧元素,创建新元素。
jsx// 旧元素 <div /> // 新元素 <span />
React 会:
- 销毁
<div>
及其所有子节点 - 创建新的
<span>
节点
- 销毁
3.4 列表比较
-
无 key 的列表:React 默认按顺序比较元素,可能导致低效更新。
jsx// 旧列表 <ul> <li>Item 1</li> // 索引0 <li>Item 2</li> // 索引1 </ul> // 新列表(插入新项) <ul> <li>Item 0</li> // 新项,索引0 <li>Item 1</li> // 旧项,索引1 <li>Item 2</li> // 旧项,索引2 </ul>
React 会:
- 更新原索引 0 的文本为 "Item 0"
- 更新原索引 1 的文本为 "Item 1"
- 创建新的 "Item 2"
-
有 key 的列表:React 使用 key 来匹配新旧元素,提高效率。
jsx// 旧列表(带key) <ul> <li key="1">Item 1</li> <li key="2">Item 2</li> </ul> // 新列表(插入新项) <ul> <li key="0">Item 0</li> // 新增 <li key="1">Item 1</li> // 保持不变 <li key="2">Item 2</li> // 保持不变 </ul>
React 会:
- 创建新的 "Item 0"(key="0")
- 保留 "Item 1" 和 "Item 2"(key 未变)
4. React 的简化策略
React 通过以下三个核心假设,将问题简化为线性时间复杂度 O (n):
2.1 **策略一:层级比较(不跨层级移动)**tree diff
- 假设:如果两个节点在新旧树中的层级不同,则直接认为它们是不同的节点,无需进一步比较。
- 复杂度优化:避免了跨层级比较的指数级组合,将比较范围限制在同一层级的节点,复杂度降为 O (n)。
2.2 策略二:组件类型相同则保留实例 component diff
- 假设 :如果两个组件类型相同(如都是
<Button />
),则认为它们是同一个组件的不同状态,只需更新 props 和状态,无需重建组件。 - 复杂度优化:避免了组件树的完全重建,将组件比较简化为属性比较,复杂度降为 O (n)。
2.3 **策略三:列表使用 key 标识唯一元素 ** element diff
- 假设:通过为列表项提供稳定的 key(如数据库 ID),可以唯一标识每个元素,即使它们在列表中的位置发生变化。
- 复杂度优化:将列表比较从 O (n²) 降为 O (n)。没有 key 时,React 默认按索引比较,插入操作可能导致后续所有元素被更新;而使用 key 后,React 可以准确识别哪些元素被添加、删除或移动。
5. 总结
React 的 diff 算法通过以下方式提高性能:
- 分层比较:避免跨层级比较,每个节点只被比较一次(O (n))
- 组件复用:相同类型组件保留实例,基于类型快速判断是否需要重建(O (n))
- key 机制:优化列表更新,使用 key 快速匹配元素(O (n))
这种简化策略的代价是:
- 不支持跨层级移动:如果真的需要移动元素到不同层级,React 会销毁并重建它们。
- key 必须稳定 :使用索引作为 key(如
key={index}
)可能导致性能问题,因为索引会随列表变化而变化。 - 子树结构变化敏感:如果在列表中间插入元素而没有 key,React 会错误地认为后续元素都发生了变化。
React Fiber 是 React 16.x 版本后引入的协调算法(Reconciler),旨在解决大型应用中渲染卡顿的问题。它的核心目标是让渲染过程更可控、更流畅。以下是其核心概念的简化说明:
React Fiber
1. 为什么需要 Fiber?
- 旧版协调器的问题:React 旧版协调器采用递归渲染(Stack Reconciler),一旦开始渲染就无法中断。如果组件树很大,可能会阻塞主线程,导致页面卡顿(尤其是在动画、滚动等交互场景中)。
- Fiber 的解决方案:将渲染任务拆分成多个 "小任务",允许中断和恢复,优先处理高优先级任务(如用户交互)。
2. 核心概念
2.1 Fiber 节点
- 定义 :每个 React 元素(如
<div>
、<App />
)在 Fiber 架构中对应一个 Fiber 节点。 - 作用:Fiber 节点不仅包含组件信息,还记录了渲染任务的优先级、状态和副作用。
2.2 任务拆分与优先级
- 渲染过程分阶段
- 协调阶段(Reconciliation):比较新旧虚拟 DOM,找出差异(可中断)。
- 提交阶段(Commit):将变化应用到真实 DOM(不可中断)。
- 优先级机制:不同类型的更新有不同优先级(如动画更新优先级高于数据加载)。
2.3 异步渲染
- 可中断 / 恢复:Fiber 允许渲染过程被暂停,优先处理高优先级任务(如用户点击),之后再恢复渲染。
- 时间分片(Time Slicing):利用浏览器的空闲时间执行渲染任务,避免长时间阻塞主线程。
3. 工作原理
3.1 链表结构
- Fiber 节点通过链表连接,形成 "树 + 链表" 结构。每个节点有三个指针:
child
:第一个子节点sibling
:下一个兄弟节点return
:父节点
3.2 工作循环(Work Loop)
-
工作单元(Work Unit):每个 Fiber 节点是一个工作单元。
-
执行过程
plaintext1. 从根节点开始,处理当前节点(计算差异) 2. 如果有子节点,处理子节点 3. 如果没有子节点,处理兄弟节点 4. 如果没有兄弟节点,返回父节点处理其兄弟节点 5. 重复直到回到根节点
-
中断条件
- 时间片用尽(浏览器需要响应用户交互)
- 更高优先级任务插入
3.3 双缓存(Double Buffering)
- React 维护两棵 Fiber 树:
- current:当前显示在屏幕上的树
- workInProgress:正在构建的新树
- 当渲染完成后,
workInProgress
会变成current
,显示在屏幕上。
4. 实际影响
4.1 用户体验优化
- 平滑动画 / 滚动:高优先级的 UI 更新不会被长时间渲染阻塞。
- 更快的交互响应:点击、输入等操作能立即得到处理。
4.2 新 API 支持
- 并发特性
useTransition
:标记低优先级更新(如数据加载)useDeferredValue
:延迟计算某些值,避免阻塞渲染
4.3 生命周期变化
- 不安全的生命周期被标记为 legacy :如
componentWillReceiveProps
、componentWillUpdate
等。 - 新增
getDerivedStateFromProps
和getSnapshotBeforeUpdate
:更安全的状态管理。
5. 总结
React Fiber 的核心是:
- 任务拆分:将渲染任务拆分成可中断的小单元
- 优先级调度:优先处理用户交互等紧急任务
- 异步渲染:利用浏览器空闲时间执行渲染,避免阻塞主线程
通过这些优化,React 在复杂应用中能保持流畅的用户体验,同时为未来的并发特性提供了基础。
React Fiber和React:diff 算法的关系
Fiber 是 Diff 算法的执行框架,而 Diff 是 Fiber 工作单元的核心逻辑之一
1. 一个简化的工作流程示例
plaintext
// 当数据变化触发渲染时:
1. Fiber 协调器开始工作
├── 创建/更新 Fiber 节点树(workInProgress 树)
│ └── 每个 Fiber 节点执行:
│ ├── 调用 Diff 算法比较新旧虚拟 DOM
│ ├── 标记副作用(如插入、更新、删除)
│ └── 继续处理下一个 Fiber 节点(可中断)
└── 所有 Fiber 节点处理完成后
└── 进入提交阶段(不可中断)
├── 将所有标记的副作用一次性应用到真实 DOM
└── 更新 current 树为最新状态
2. 总结
- React Fiber 是一种架构设计,为 Diff 算法提供了更灵活的执行环境(如异步、可中断、优先级调度)。
- Diff 算法 是 Fiber 工作单元的核心逻辑之一,负责计算虚拟 DOM 的差异。
- 两者结合 使得 React 能够在复杂应用中保持高性能和流畅的用户体验。