React 原理详解
React 是当今最流行的前端框架之一,它的设计理念和实现原理值得深入学习。本文将从核心概念出发,详细解析 React 的工作原理,包括 Fiber 架构、渲染流程、diff 算法、key 的作用、批处理机制以及并发模式等关键内容,帮助你彻底理解 React 的内部工作机制。
一、Fiber 是什么
1.1 Fiber 的定义
Fiber 是 React 16 引入的一种新的核心数据结构,它是对 React 组件树的一种抽象表示,用于实现可中断的渲染过程和优先级调度。
通俗理解:
想象传统的 React 渲染是一条直线,一旦开始就不能停止,直到完成整个渲染过程。而 Fiber 则将渲染过程拆分成一个个可以中断的小任务,就像一条链条,每一节都是一个独立的工作单元。
1.2 Fiber 的核心概念
| 概念 | 说明 |
|---|---|
| Fiber 节点 | 对应一个组件实例或 DOM 元素,包含类型、属性、状态等信息 |
| Fiber 树 | 由 Fiber 节点构成的树状结构,是 React 内部的虚拟 DOM 表示 |
| Fiber 调度 | 基于优先级的任务调度机制,可中断和恢复 |
| Fiber 工作循环 | 分为 render 阶段和 commit 阶段 |
1.3 Fiber 解决的问题
传统 React 16 之前的问题:
- 渲染过程不可中断:一旦开始渲染,会一直占用主线程,导致用户交互卡顿
- 调度不灵活:无法根据任务优先级调整执行顺序
- 内存占用高:递归调用栈深度可能很高
Fiber 带来的改进:
- 可中断的渲染:将渲染过程拆分成小任务,可随时中断
- 优先级调度:根据任务优先级调整执行顺序
- 更好的错误处理:局部错误不会影响整个应用
- 支持并发模式:为 React 18 的并发特性奠定基础
1.4 Fiber 节点结构
1 | // Fiber 节点的核心结构 |
1.5 Fiber 树的构建过程
- 初始化:创建根 Fiber 节点
- 遍历:深度优先遍历组件树,为每个组件创建对应的 Fiber 节点
- 标记:标记需要更新的节点(添加、删除、更新)
- 提交:将标记的变更应用到真实 DOM
二、渲染流程
2.1 传统渲染流程 vs Fiber 渲染流程
传统渲染流程(React 15):
1 | ┌─────────────┐ ┌─────────────┐ |
Fiber 渲染流程(React 16+):
1 | ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ |
2.2 渲染流程详解
阶段一:调度阶段(Scheduler)
核心任务:
- 接收更新请求(setState、useState 等)
- 计算任务优先级
- 安排任务执行顺序
关键步骤:
- 任务入队:将更新任务加入调度队列
- 优先级计算:根据任务类型和时间计算优先级
- 任务调度:选择最高优先级的任务执行
阶段二:渲染阶段(Render)
核心任务:
- 构建 Fiber 树
- 标记需要更新的节点
- 收集副作用
关键步骤:
- 创建 WorkInProgress 树:基于 Current 树创建
- 深度优先遍历:
- 计算新的状态和 props
- 比较新旧节点,标记变更
- 收集副作用(effects)
- 完成渲染:生成 Effect 链表
注意: 渲染阶段是可中断的,如果有更高优先级的任务,会暂停当前渲染。
阶段三:提交阶段(Commit)
核心任务:
- 将渲染阶段的变更应用到真实 DOM
- 执行副作用(如 useEffect)
- 更新 ref
关键步骤:
- 准备提交:获取 Effect 链表
- DOM 变更:
- 执行 DOM 插入、更新、删除
- 更新 ref
- 副作用执行:
- 执行 useEffect cleanup
- 执行 useEffect 回调
- 完成提交:将 WorkInProgress 树设置为 Current 树
注意: 提交阶段是不可中断的,必须一次性完成。
2.3 渲染流程示意图
1 | ┌─────────────────────────────────────────────────────────────────┐ |
2.4 生命周期与渲染流程的关系
旧生命周期(React 16.3 之前):
componentWillMount→ 渲染阶段开始前componentWillReceiveProps→ 渲染阶段shouldComponentUpdate→ 渲染阶段(决定是否继续)componentWillUpdate→ 渲染阶段render→ 渲染阶段componentDidMount→ 提交阶段后componentDidUpdate→ 提交阶段后componentWillUnmount→ 提交阶段(删除时)
新生命周期(React 16.3+):
getDerivedStateFromProps→ 渲染阶段开始shouldComponentUpdate→ 渲染阶段(决定是否继续)render→ 渲染阶段getSnapshotBeforeUpdate→ 提交阶段开始前componentDidMount→ 提交阶段后componentDidUpdate→ 提交阶段后componentWillUnmount→ 提交阶段(删除时)componentDidCatch→ 错误边界
三、diff 算法策略
3.1 diff 算法的核心思想
diff 算法是 React 用来比较新旧虚拟 DOM 树,找出需要更新的节点的算法。
核心原则:
- 只比较同级节点:不跨层级比较
- key 是标识:使用 key 来识别节点
- 类型不同则替换:节点类型不同,直接替换整个子树
- 类型相同则更新:节点类型相同,更新属性和子节点
3.2 diff 算法的优化策略
策略一:分层比较
1 | ┌─────────────┐ ┌─────────────┐ |
优势: 时间复杂度从 O(n³) 降低到 O(n)
策略二:key 识别
1 | 旧节点:[A(key:1), B(key:2), C(key:3)] |
优势: 避免不必要的 DOM 操作,提高性能
策略三:类型判断
- 类型相同:更新属性,递归比较子节点
- 类型不同:直接替换整个子树
1 | if (oldNode.type !== newNode.type) { |
3.3 diff 算法的具体实现
单节点 diff
处理逻辑:
- 比较 key 是否相同
- 比较 type 是否相同
- 根据比较结果执行相应操作
情况分析:
| 情况 | key | type | 操作 |
|——|—–|——|——|
| 1 | 相同 | 相同 | 更新属性 |
| 2 | 相同 | 不同 | 替换节点 |
| 3 | 不同 | - | 创建新节点 |
多节点 diff
处理逻辑:
- 第一轮:从头部开始比较,相同则继续,不同则停止
- 第二轮:从尾部开始比较,相同则继续,不同则停止
- 第三轮:处理剩余节点
- 新节点多:创建新节点
- 旧节点多:删除多余节点
- 节点移动:通过 key 查找位置并移动
示例:
1 | 旧节点:A B C D E |
3.4 diff 算法的时间复杂度
理想情况: O(n) —— 只需要遍历一次节点
最坏情况: O(n²) —— 所有节点都需要移动位置
实际情况: 由于使用了 key 和分层比较,通常接近 O(n)
四、key 的作用原理
4.1 key 的定义
key 是 React 用来识别组件的唯一标识符,在列表渲染时尤为重要。
key 的要求:
- 唯一性:在兄弟节点中必须唯一
- 稳定性:不随渲染而变化
- 可预测性:最好是稳定的 ID,而不是索引
4.2 key 的作用
作用一:识别节点
有 key 时:
- React 可以通过 key 快速识别哪些节点是相同的
- 避免不必要的 DOM 操作
- 保持组件状态
无 key 时:
- React 会使用索引作为默认 key
- 可能导致组件状态混乱
- 性能下降
作用二:优化渲染性能
例子:
1 | // 不好的做法(使用索引) |
性能差异:
- 当列表顺序变化时,使用索引会导致所有节点重新渲染
- 使用唯一 ID 只需要移动节点位置
4.3 key 的工作原理
在 diff 过程中的作用:
- 创建 key-index 映射:React 会为旧节点创建 key 到索引的映射
- 查找匹配节点:通过 key 快速查找新节点在旧树中的位置
- 确定操作类型:根据查找结果确定是更新、移动还是创建
源码层面:
1 | // 简化的 key 查找逻辑 |
4.4 key 的最佳实践
推荐做法:
- 使用后端返回的唯一 ID
- 确保 key 在兄弟节点中唯一
- 不要使用随机值作为 key
- 不要在不同列表中使用相同的 key
避免做法:
- 使用索引作为 key(特别是列表会排序或筛选时)
- 使用 Math.random() 作为 key
- 在不同组件间复用相同的 key
五、React 18 批处理
5.1 批处理的定义
批处理(Batching) 是 React 将多个状态更新合并为一次渲染的机制,以提高性能。
通俗理解:
想象你去超市购物,一次买完所有东西再结账,比买一件结一次账要高效得多。React 的批处理就是类似的道理。
5.2 批处理的演进
React 17 及之前:
- 仅在事件处理器内批处理:异步代码(setTimeout、Promise 等)中的更新不会被批处理
React 18:
- 自动批处理:无论在何处触发的更新都会被批处理
- 更广泛的批处理:包括 setTimeout、Promise、原生事件等
5.3 批处理的工作原理
核心机制:
- 收集更新:将多个 setState 调用收集到队列中
- 延迟执行:不立即执行更新,而是等待当前事件循环结束
- 合并更新:将多个更新合并为一次渲染
- 执行渲染:只进行一次 DOM 更新和渲染
代码示例:
1 | // React 17:只有事件处理器内的更新会被批处理 |
5.4 批处理的优势
| 优势 | 说明 |
|---|---|
| 减少渲染次数 | 合并多次更新为一次,减少 DOM 操作 |
| 提高性能 | 减少浏览器重绘和回流 |
| 保持 UI 一致性 | 避免中间状态的闪烁 |
| 简化代码 | 不需要手动管理更新时机 |
5.5 如何退出批处理
使用 flushSync:
1 | import { flushSync } from 'react-dom'; |
适用场景:
- 需要立即看到更新结果的场景
- 与非 React 库集成时
- 特定的性能优化场景
六、并发模式基本概念
6.1 并发模式的定义
并发模式(Concurrent Mode) 是 React 18 引入的一种新的渲染模式,它允许 React 同时处理多个任务,并根据优先级调整执行顺序。
通俗理解:
传统模式下,React 一次只做一件事,直到完成。并发模式下,React 可以同时做多个事情,并且可以根据重要性调整顺序。
6.2 并发模式的核心概念
| 概念 | 说明 |
|---|---|
| 优先级 | 任务的重要程度,决定执行顺序 |
| 时间切片 | 将渲染任务分成小片段,避免阻塞主线程 |
| 可中断渲染 | 渲染过程可以被更高优先级的任务中断 |
| Suspense | 允许组件在数据加载完成前显示 fallback |
6.3 并发模式的优势
| 优势 | 说明 |
|---|---|
| 更流畅的用户体验 | 高优先级任务(如用户输入)可以优先执行 |
| 减少卡顿 | 渲染过程可中断,不会阻塞主线程 |
| 更好的资源利用 | 可以在空闲时间处理低优先级任务 |
| 支持 Suspense | 简化数据加载的处理 |
6.4 并发模式的工作原理
核心机制:
- 任务调度:根据优先级调度任务
- 时间切片:将渲染拆分成 16ms 以内的小任务
- 可中断渲染:允许高优先级任务中断低优先级任务
- 自动批处理:合并更新以提高性能
执行流程:
1 | ┌─────────────────────────────────────────────────────────────────┐ |
6.5 并发模式的 API
开启并发模式:
1 | // React 18 之前 |
Suspense:
1 | <Suspense fallback={<Loading />}> |
useTransition:
1 | import { useTransition } from 'react'; |
useDeferredValue:
1 | import { useDeferredValue } from 'react'; |
6.6 并发模式的应用场景
| 场景 | 优势 |
|---|---|
| 用户输入 | 输入时不会卡顿,响应更及时 |
| 数据加载 | 可以显示加载状态,避免空白页面 |
| 大型列表 | 滚动时更流畅,不会阻塞主线程 |
| 动画效果 | 动画更平滑,不受渲染影响 |
七、React 原理总结
7.1 核心概念关系图
1 | ┌─────────────────────────────────────────────────────────────────┐ |
7.2 关键原理总结
| 概念 | 核心要点 |
|---|---|
| Fiber | 可中断的渲染单元,支持优先级调度 |
| 渲染流程 | 调度 → 渲染 → 提交,渲染阶段可中断 |
| diff 算法 | 分层比较、key 识别、类型判断 |
| key | 唯一标识符,优化渲染性能,保持组件状态 |
| 批处理 | 合并更新,减少渲染次数,提高性能 |
| 并发模式 | 优先级调度,时间切片,可中断渲染 |
7.3 学习建议
- 理解核心概念:Fiber、渲染流程、diff 算法是基础
- 实践验证:通过调试工具观察渲染过程
- 对比学习:与其他框架(如 Vue)的渲染机制对比
- 关注版本变化:React 18 的并发特性是重点
- 阅读源码:深入理解 React 的实现细节
7.4 常见面试题
Q:Fiber 是什么?它解决了什么问题?
Fiber 是 React 16 引入的核心数据结构,它将渲染过程拆分成可中断的小任务,支持优先级调度,解决了传统 React 渲染过程不可中断导致的卡顿问题。
Q:React 的渲染流程是怎样的?
渲染流程分为三个阶段:调度阶段(接收更新,计算优先级)、渲染阶段(构建 Fiber 树,标记变更)、提交阶段(应用 DOM 变更,执行副作用)。渲染阶段是可中断的,提交阶段不可中断。
Q:diff 算法的优化策略有哪些?
主要优化策略包括:分层比较(只比较同级节点)、key 识别(使用 key 快速定位节点)、类型判断(类型不同直接替换)。
Q:key 的作用是什么?为什么不能使用索引作为 key?
key 是 React 用来识别组件的唯一标识符,它能帮助 React 快速定位需要更新的节点,避免不必要的 DOM 操作。使用索引作为 key 在列表排序或筛选时会导致组件状态混乱,因为索引会随位置变化而变化。
Q:React 18 的批处理有什么改进?
React 18 引入了自动批处理,无论在何处触发的更新(包括 setTimeout、Promise 等)都会被批处理,而 React 17 及之前只在事件处理器内批处理。
Q:并发模式的核心特性是什么?
并发模式的核心特性包括:优先级调度、时间切片、可中断渲染、Suspense 支持。它允许 React 同时处理多个任务,并根据优先级调整执行顺序,提供更流畅的用户体验。
八、总结
React 的设计理念和实现原理是前端开发中的重要知识,掌握这些原理不仅能帮助你写出更高效的 React 代码,还能让你在面试中脱颖而出。
核心要点:
- Fiber 是 React 16+ 的核心数据结构,支持可中断渲染
- 渲染流程 分为调度、渲染、提交三个阶段
- diff 算法 通过分层比较、key 识别等策略优化性能
- key 是优化渲染的关键,应使用唯一稳定的标识符
- React 18 引入了自动批处理和并发模式
- 并发模式 提供了更好的用户体验和性能
希望本文能帮助你彻底理解 React 的工作原理,在前端开发中更加游刃有余!