React Fiber:重构 React 的渲染引擎
如果你只把 React 当作一个 UI 库来用,你可能永远不会关心 Fiber。但如果你想构建一个流畅的、高交互的现代 Web 应用,理解 Fiber 是在理解 React 为何能同时做到“声明式”与“高性能”。
协调器、渲染器、工作循环 正是 Fiber 架构的三大支柱。下面我将从“Fiber 是什么?解决了什么问题?”开始,深入 Fiber 的本质、工作流程,以及它如何支撑起 Concurrent Mode(并发模式)。
1. 背景:旧调和器(Stack Reconciler)的“不可中断”之痛
在 React 15 及之前,调和器(Stack Reconciler)采用递归同步的方式遍历虚拟 DOM 树。这个过程就像函数调用栈:一旦开始渲染,就会一直递归到最深层的子节点,然后再返回。整个更新过程无法被中断。
这意味着:当组件树很庞大时,主线程会被 React 的渲染工作长时间占用,阻塞浏览器主线程。此时用户的点击、输入、动画等交互无法得到响应,导致掉帧、卡顿,甚至浏览器假死。
核心问题:旧的渲染模型是“同步的、递归的、不可中断的”。
2. Fiber 是什么?
Fiber 是 React 16 引入的新的协调(Reconciliation)引擎,完全重写了之前的 Stack Reconciler。
Fiber 通过 可中断的增量渲染、时间切片 和 任务优先级,将渲染工作拆分为多个小任务,允许浏览器在空闲时执行,从而实现流畅的用户体验。
Fiber 不仅仅是一个数据结构。Fiber 有三层含义:
2.1 作为一种数据结构
每个 Fiber 节点对应一个 React 元素(组件、DOM 节点等),它是一个 链表结构的 JavaScript 对象。典型的 Fiber 节点包含:
type:组件类型("div"、MyComponent)key:用于列表 diffchild:指向第一个子 Fibersibling:指向下一个兄弟 Fiberreturn:指向父 Fiberalternate:双缓冲相关。指向另一棵树的对应节点(current 或 workInProgress)stateNode:对应的真实 DOM 节点或组件实例pendingProps/memoizedProps:新旧 propsmemoizedState:上次渲染的状态updateQueue:存放待处理的更新flags(旧称effectTag):标记这个节点需要执行什么操作(增、删、改)lanes:优先级
这种链表结构使得 遍历过程可以被暂停和恢复。因为只需要保留当前节点的指针,下次从中断处继续即可。
2.2 作为一种执行单元
Fiber 也是一个 工作单元(Unit of Work)。在 React 内部,每次更新会被拆分成多个小任务,每个任务处理一个 Fiber 节点。任务可以按优先级插入队列,由调度器(Scheduler)统一管理。
2.3 作为一种架构理念
Fiber 架构重新设计了 React 的运行时模型,把原本单一的递归调用栈,变成了一个可以在多帧中分批执行的工作循环。
3. Fiber 解决了什么问题?—— 时间切片 + 优先级调度
Fiber 带来的核心能力是:可中断、可恢复、可优先级的异步渲染。
-
时间切片:React 将长时间渲染任务切分成多个小任务,每个任务执行时间控制在约 5ms(一帧的预算内),并在每小任务后主动让出主线程。这样浏览器就有机会响应用户输入、动画等。
Fiber 的支持方式:
- 渲染工作被拆分为每个 Fiber 节点一个工作单元。
- 工作循环(workLoop)每次处理一个单元,之后检查是否超出时间预算。
- 如果剩余时间不足(如 <1ms),则调用 yield 终止循环,通过调度器发起下一次继续。
- 调度器利用 MessageChannel 或 requestIdleCallback 在帧空闲时继续执行
-
优先级调度:不同的更新可以拥有不同的优先级。例如:
- 离散事件(如用户点击、输入):最高优先级 ImmediatePriority
- 连续事件(如滚动、动画):高优先级 UserBlockingPriority
- 数据请求、过渡更新:普通优先级 NormalPriority
- 预渲染、后台任务:低优先级 LowPriority
- 闲置任务:IdlePriority
-
并发模式(Concurrent Mode):React 可以在内存中同时准备多个版本的 UI,等合适的时候再提交到屏幕。这是 Suspense、useTransition、useDeferredValue 等特性的底层基础。
结合上方内容,一句话总结:Fiber是一个新的协调机制,解决了React15及之前的同步递归不可中断、缺乏优先级调度、无法在渲染过程中做额外工作等问题,通过可中断的增量渲染、时间切片 和 任务优先级,将渲染工作拆分为多个小任务,允许浏览器在空闲时执行,从而实现流畅的用户体验。。
4. Fiber 的工作流程(核心架构与工作原理)
整个工作流程分为两大阶段:Render 阶段 和 Commit 阶段。
4.1 Render 阶段 —— 可中断的构建
这个阶段的目标:构建 Fiber 树(workInProgress tree),并找出哪些节点发生了变化(收集 Effect)。
- 入口:
performSyncWorkOnRoot或performConcurrentWorkOnRoot - 核心函数:
workLoopSync/workLoopConcurrent - 过程:
-
从根 Fiber 开始,深度优先遍历(先 child 后 sibling)。
-
对每个 Fiber 节点调用
beginWork(递的过程):根据组件类型(FunctionComponent, ClassComponent, HostComponent 等)执行对应的 diff 和更新逻辑,生成新的子 Fiber。
-
当遍历到叶子节点(没有 child)时,调用
completeWork(归的过程):收集副作用(如 DOM 操作、生命周期调用),并向上冒泡。
如果有兄弟节点,怎进入到兄弟节点的递阶段;如果不存在兄弟节点,那就会进入到父节点Fiber的归阶段
-
每处理完一个 Fiber 节点,检查是否超时(
shouldYield)。在并发模式下,如果超出时间片,则暂停循环,保存当前进度,把控制权交还给浏览器。下一帧恢复时,从中断的地方继续。
-
这个阶段构建的 workInProgress 树是一个双缓冲(double buffering)结构,与当前的 current 树隔离。所有的工作都在内存中进行,用户看到的内容不会改变。
调和就放生在这个阶段。调和是 React 比较新旧虚拟 DOM 树(Fiber 树)并确定最小变更集的过程。
调和的核心算法是 Diffing,主要规则如下:
① 同层比较: 只比较同一层级的节点,不跨层级移动。
② 不同类型的组件: 如果两个节点类型不同(如 div → p),React 会销毁旧子树(执行卸载生命周 期)并创建新子树,不会尝试对比子节点。
③ 相同类型的DOM元素:对比属性(className、style 等),只更新变化的属性。
④ 相同类型的组件: 组件实例保持不变,更新 props 并调用生命周期(如 componentWillReceiveProps), 递归对比子节点。
⑤ 列表 key 优化:对于子节点列表,使用 key 属性来识别哪些元素被移动、添加或删除,避免低 效的逐个比较。
调和在 Render 阶段执行,基于类型、key、层级等规则高效比较新旧 Fiber 树,标记出需要更新的 DOM 节点。
4.2 Commit 阶段 —— 同步且不可中断
一旦 Render 阶段完成,得到了一棵完整的 workInProgress 树,以及一条 Effect 链表(记录了哪些节点需要增、删、改,哪些 Ref 需要更新,哪些生命周期需要调用),就进入 Commit 阶段。
Commit 阶段的核心工作由 commitRoot 函数驱动,它分为三个子步骤:
-
before mutation:DOM 变更前。- 调用
getSnapshotBeforeUpdate,读取 DOM 状态,让组件在 DOM 变更前获取旧值(如滚动位置)。执行清理函数。
- 调用
-
mutation(DOM 变更):DOM 变更。直接操作真实 DOM(插入、删除、更新属性)。这个阶段是同步的,因为需要保证 DOM 变更的原子性。- 对于
Placement:调用appendChild / insertBefore添加节点。 - 对于
Update:更新 DOM 属性(class、style、事件监听等)。 - 对于
Deletion:调用componentWillUnmount或useEffect清理函数,然后移除节点。 - 在此阶段,会调用上一次
useLayoutEffect的清理函数(即清除之前的 effect,以避免状态不一致。)
- 对于
-
layout(commit 后):DOM 变更后、浏览器绘制前- 调用
componentidMount / componentDidUpdate(类组件); - 同步调用所有的
useLayoutEffect的回调函数(新 effect); - 调度
useEffect的回调(放入任务队列,等待浏览器空闲执行); - 更新
refs等。
- 调用
Commit 结束后,workInProgress 树成为新的 current 树,等待下一次更新。浏览器会进行重绘(Paint),用户看到最终界面。
🤔 为什么不能中断?
因为浏览器一旦开始绘制,中间状态的更改会导致 UI 不一致。所以所有 DOM 变更必须一次性同步完成,才能保证界面一致性和生命周期语义的正确性。
Commit 阶段必须是同步且不可中断,主要基于以下原因:
① 用户可见的一致性:Commit 阶段直接操作真实 DOM,如果在中间被打断,用户可能看到部分更新的界面(例如半个列表被插入),造成视觉不一致或状态损坏。
② 浏览器 API 不支持中断:一旦开始调用 appendChild、removeChild、setAttribute 等 DOM 变更方法,无法“回滚”或“暂停”。如果中断,后续恢复时无法确定当前 DOM 的状态。
③ 依赖同步执行的生命周期:componentDidMount、useLayoutEffect、getSnapshotBeforeUpdate等期望在 DOM 变更后立即执行,以进行布局测量或同步修正。异步化会使它们的行为难以预测。
④ 避免竞态条件:多个更新如果同时进入 Commit 阶段,需要保证最终 DOM 状态与最后一次更新一致。同步执行可以确保顺序性和原子性。
因此,React 的设计是:Render 阶段可中断,负责计算;Commit 阶段不可中断,负责应用。
🤔 在这里也经常被问到:“useEffect 和 useLayoutEffect 分别在哪个阶段执行?”
用这个时序图就可以一目了然:
Render 阶段(计算副作用,但不应用)
↓
Commit 阶段开始
├── Before Mutation(调用 getSnapshotBeforeUpdate)
├── Mutation(执行 DOM 操作)
│ └── 在此阶段,会调用上一次 useLayoutEffect 的清理函数(即清除之前的 effect)
├── Layout(同步调用所有 useLayoutEffect 的回调函数)
└── Commit 阶段结束
↓
浏览器执行绘制(Paint),用户看到更新后的界面
↓
(之后空闲时)异步调用 useEffect 的回调
useLayoutEffect:在 Commit 阶段同步执行。 具体时机是 React 完成所有 DOM 变更后、浏览器执行绘制(Paint)之前。它可用于读取布局(如 getBoundingClientRect)或同步修改 DOM 以避免闪烁。因为它会阻塞浏览器绘制,过度使用可能导致性能问题。
useEffect:在 Commit 阶段之后异步调度。 React 将所有 useEffect 函数收集起来,在浏览器绘制(Paint)之后,在浏览器空闲时通过 Scheduler (或下一次 Paint 之后)执行。不会阻塞屏幕更新,适用于数据获取、订阅、日志等大多数副作用。
5. 协调器、渲染器与工作循环的协作
- 协调器(Reconciler):负责 Fiber 的 Diff 与标记更新。它不关心宿主环境(DOM、Native、Canvas 等),只负责生成 Effect。也就是上面描述的 Render 阶段的工作。
- 渲染器(Renderer):负责把 Effect 应用到具体宿主环境。例如
react-dom负责操作 DOM,react-native负责调用原生视图接口。也就是上面描述的 Commit 阶段。 - 工作循环(Work Loop):即
workLoop函数,它是整个 Fiber 架构的驱动器。它决定何时开始一个任务、何时暂停、何时恢复、如何调度下一个任务。在 React 内部,由 Scheduler(调度器) 配合实现,调度器独立于 React,提供了类似requestIdleCallback但更强大的能力(如优先级、延时任务)。
核心记忆点
- 调度器:负责“什么时候做” —— 优先级 + 时间切片。
- 协调器:负责“做什么改变” —— diff + 标记 effect。
- 渲染器:负责“真正去做” —— 操作 DOM + 生命周期。
6. Lane 模型 —— 更精细的优先级
优先级工作方式:
- React 17+ 采用 lane 优先级模型(二进制位运算的优先级系统),每个 Fiber 节点及更新(update)都带有一个或多个 Lane(例如 SyncLane、InputContinuousLane、DefaultLane、IdleLane)
- 调度器维护一个任务队列,每次选择最高优先级的任务执行。
- 当高优先级任务打断低优先级任务时,低优先级任务会被挂起,其工作进度保留(workInProgress 树不丢弃),高优先级完成后,再基于原有进度重新恢复低优先级任务(可能丢弃部分已做工作)。
- 通过饥饿避免机制:长时间未执行的低优先级任务会被“提升”优先级,防止一直被饿死(确保低优先级更新的任务不会无限期被挂起。)
7. 总结:Fiber 带来的不仅仅是性能
| 维度 | React 15 (Stack) | React 16+ (Fiber) |
|---|---|---|
| 遍历方式 | 递归,不可中断 | 循环链表,可中断/恢复 |
| 任务拆分 | 整个树为一个大任务 | 每个节点为一个工作单元 |
| 时间片 | 无 | 约 5ms |
| 优先级 | 无 | Lane 模型,细粒度优先级 |
| 并发渲染 | 不支持 | 支持(Concurrent Mode) |
| 适用场景 | 中小型应用 | 大型、交互复杂的应用 |
Fiber 架构并没有让 React 的核心编程模型(JSX、组件、state、props)发生任何改变,它是一次彻底的底层重构,让 React 在保持“声明式 UI”优点的同时,获得了接近“命令式性能优化”的能力。