React Fiber 并发调度机制
React Fiber 并发调度机制:优先级、Lane 与时间切片
引言
如果你曾疑惑「React 18 的并发模式到底在并发什么」,那么答案是:它并发的是「计算」,而不是「UI」。React Fiber 架构的真正野心,是让渲染这件曾经「一鼓作气」的原子操作,变成可以随时暂停、插队、恢复的协作式任务流。本文将从数据结构、优先级模型、调度算法三个层面,把并发调度彻底讲透。
阅读本文前,建议先掌握 React 渲染管线的基础概念(可参考本站《React 渲染流程深度剖析》)。本文将聚焦于「渲染如何进行优先级排序」以及「时间切片如何落地」两大核心问题,全文超过五千字,包含六张流程图。
一、从同步渲染到并发渲染的演进
React 15 及更早版本采用同步递归渲染:一次更新从根节点一路递归到叶子,期间主线程被完全占用。这种模型的致命缺陷在于——渲染一旦开始就无法停止。当应用足够复杂(数千个组件)时,一次渲染可能耗时上百毫秒,期间用户点击、输入全部被阻塞,表现为明显的掉帧与卡顿。
为什么同步渲染会卡顿
flowchart TB
classDef sA fill:#ffebee,stroke:#c62828,color:#b71c1c
classDef sB fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef sC fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[用户点击按钮] --> B[setState 入队]
B --> C[同步渲染开始]
C --> D[递归遍历整棵组件树]
D --> E{主线程占用时长}
E -->|短 <16ms| F[绘制完成 无感知]
E -->|长 >100ms| G[期间用户输入被阻塞]
G --> H[掉帧/白屏/点击无响应]
G --> I[动画卡顿]
click C "https://github.com/facebook/react/blob/main/packages/react-reconciler/README.md" "reconciler 说明"
class A sA
class B sA
class C sA
class D sB
class E sB
class F sC
class G sA
class H sA
class I sA
协作式调度的目标
React 团队给出的答案是:把渲染工作切分成细粒度的工作单元(Fiber),每完成一部分就检查主线程的「剩余时间」,若预算耗尽则主动让出,等浏览器有空闲帧再继续。整个过程由调度器(Scheduler)统一管理,这便是「并发」的本质——不是多线程并行,而是在单线程内交错执行多个优先级不同的任务。
二、Fiber 节点:可中断的最小工作单元
要实现可中断渲染,首先要把「递归调用栈」替换为「显式的链表」。Fiber 节点的设计正是为此而生:它存储了组件的全部信息,并通过指针串联成树,调度器可以在任意节点暂停并保存现场。
单链表树结构
flowchart TB
classDef fA fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef fB fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef fC fill:#e0f7fa,stroke:#00838f,color:#004d40
R[RootFiber] --> A[Fiber: App 组件]
A --> B[Fiber: div#root]
B --> C[Fiber: header]
B --> D[Fiber: main]
C --> E[Fiber: nav]
C --> F[Fiber: h1]
D --> G[Fiber: section]
D --> H[Fiber: aside]
A -. return .-> R
B -. return .-> A
C -. return .-> B
D -. return .-> B
E -. return .-> C
F -. return .-> C
G -. return .-> D
H -. return .-> D
C -. sibling .-> D
E -. sibling .-> F
G -. sibling .-> H
click B "https://react.dev/learn/understanding-your-ui-as-a-tree" "UI 树概念"
class A fA
class B fB
class C fC
class D fC
class E fC
class F fC
class G fC
class H fC
class R fA
每个 Fiber 都维护了以下核心字段:
tag:节点类型(函数组件、类组件、HostComponent 等);type与key:用于 Diff 与复用判断;pendingProps与memoizedProps:新旧属性快照;stateNode:关联的真实 DOM 或类组件实例;child、sibling、return:树遍历的三指针;lanes与childLanes:该节点参与的更新优先级;alternate:指向双缓存树中的对应节点。
beginWork 与 completeWork
调和阶段遍历这棵链表树时,React 采用「深度优先 + 回溯」策略:beginWork 负责进入节点时的工作(Diff、计算 props、调用函数组件),completeWork 负责节点子树完成后汇总副作用。这种「下山式」遍历替代了原生调用栈,使中断与恢复成为可能。
sequenceDiagram
participant S as Scheduler
participant B as beginWork
participant C as completeWork
participant L as Lane 标记
S->>B: 处理节点 A(进入)
B->>B: Diff 属性与子节点
B->>C: 返回子节点指针
C->>C: 汇总子节点副作用
C->>B: 回溯处理兄弟节点
B->>S: 本轮时间预算用尽
S->>S: 记录工作进度(保存遍历栈)
S->>B: 空闲帧恢复遍历
C->>L: 全部完成 标记提交 Lane
三、优先级模型:Lane 车道系统
React 17 时代使用数字优先级(PriorityLevel 0~4),粒度过粗。React 18 彻底重构为 Lane 模型:用一个 32 位二进制数(或 31 位的 Lanes)表示「同时存在多条更新」,每一位代表一条车道,数值越小优先级越高。
车道与任务的对应关系
flowchart LR
classDef lA fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
classDef lB fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef lC fill:#ffebee,stroke:#c62828,color:#b71c1c
classDef lD fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
A[SyncLane 同步车道] --> B[最高优先级<br/>flushSync 强制同步]
C[InputDiscreteLane<br/>离散输入车道] --> D[点击/输入/按键<br/>必须尽快响应]
E[InputContinuousLane<br/>连续输入车道] --> F[滚动/拖拽<br/>保持流畅]
G[DefaultLane 默认车道] --> H[普通 setState<br/>常规更新]
I[TransitionLane<br/>过渡车道] --> J[useTransition<br/>可延迟的非紧急更新]
K[IdleLane 空闲车道] --> L[预取/缓存类任务<br/>最后执行]
B --> M[Lanes 汇总运算]
D --> M
F --> M
H --> M
J --> M
L --> M
M --> N{取最高优先级车道}
N --> O[进入调度]
click J "https://react.dev/reference/react/useTransition" "useTransition 说明"
click I "https://react.dev/reference/react/useTransition" "Transition 车道"
class A lC
class C lC
class E lB
class G lB
class I lD
class K lA
class N lD
为什么用位运算表示优先级
Lane 模型最强大的地方在于集合运算。一次渲染可能同时包含多条更新,React 需要快速回答三个问题:
- 当前更新属于哪条车道? ——
lanes & lane !== 0 - 多个任务谁先执行? —— 取
getHighestPriorityLane(lanes),即最小的非零位 - 该车道的任务还能不能等? —— 通过过期时间
expirationTime判断是否已过期,过期则升级为不可延迟
位运算的常数级复杂度,让 React 在每次 Fiber 遍历中都能以纳秒级开销完成优先级判断,这是调度性能的基础保障。
四、调度器的工作流程
Scheduler 模块(独立发布的 scheduler 包)负责把渲染任务安排在正确的时机执行。它的核心是一个基于 MessageChannel 的最小堆任务队列。
调度循环
flowchart TB
classDef qA fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef qB fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef qC fill:#fff3e0,stroke:#f57c00,color:#e65100
classDef qD fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[scheduleCallback<br/>任务入队] --> B[最小堆按过期时间排序]
B --> C[requestHostCallback<br/>MessageChannel 触发]
C --> D[workLoop 开始]
D --> E{堆顶任务是否过期}
E -->|是| F[同步执行该任务]
E -->|否| G{当前帧剩余时间}
G -->|还有时间| H[执行任务时间片]
H --> I{任务是否完成}
I -->|未完成| J[记录中断 重新入堆]
I -->|完成| K[弹出任务]
G -->|无时间| L[让出主线程 等下一帧]
J --> D
K --> D
L --> C
click A "https://github.com/facebook/react/tree/main/packages/scheduler" "scheduler 源码"
class A qA
class B qB
class C qB
class D qA
class E qC
class F qD
class G qC
class H qD
class I qD
class J qC
class K qD
class L qC
时间切片的具体实现
浏览器没有「剩余时间」的原生 API(requestIdleCallback 优先级太低且不兼容),因此 React 自研了一套方案:
requestAnimationFrame获取当前帧开始时间;MessageChannel的postMessage在宏任务阶段尽早触发回调;- 设定
frameInterval(默认 5ms)为单次工作预算; - 用
performance.now()检测实际耗时,超出预算即返回,shouldYield返回 true。
pie showData
title 一帧 16.6ms 的时间分配
"渲染工作时间片 (5ms)" : 5
"浏览器布局与绘制" : 8
"输入响应与空闲缓冲" : 3.6
五、并发特性在应用层的落地
了解了调度内核,再看 React 18 暴露给开发者的并发 API,一切豁然开朗。
useTransition 与 useDeferredValue
flowchart LR
classDef tA fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
classDef tB fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
classDef tC fill:#e0f7fa,stroke:#00838f,color:#004d40
classDef tD fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[用户在搜索框输入] --> B{useTransition 包裹}
B -->|是| C[输入为同步高优先级]
C --> D[结果列表为过渡低优先级]
D --> E[输入流畅不卡顿]
B -->|否| F[输入与列表同优先级]
F --> G[列表渲染阻塞输入]
G --> H[输入掉帧]
D --> I{useDeferredValue 场景}
I --> J[图表渲染昂贵 但可延迟]
J --> K[展示旧值 后台计算新值]
click D "https://react.dev/reference/react/useTransition" "Transition 概念"
click K "https://react.dev/reference/react/useDeferredValue" "DeferredValue 说明"
class A tA
class B tA
class C tC
class D tB
class E tD
class F tA
class G tC
class H tD
class I tB
class J tB
class K tD
实战要点:
useTransition的isPending可用于展示加载占位,避免白屏闪烁;useDeferredValue适合「渲染昂贵但结果可以晚一点展示」的场景,如大数据筛选;- 不要把紧急操作(表单提交、路由跳转)放进 transition,会延迟用户反馈;
- 并发特性在并发模式下默认开启,
StrictMode会在开发环境执行双调用以暴露不纯函数。
六、中断恢复与副作用安全
并发渲染最微妙的问题在于:被中断的渲染可能永远不会完成。如果组件函数有副作用(如直接修改外部变量、建立订阅后未清理),中断后重跑可能导致状态错乱。因此 React 施加了严格的纪律:
- 组件函数必须纯:相同 props 必须返回相同结果;
- 副作用必须进 Effects:
useEffect/useLayoutEffect只在提交阶段执行,天然不会被中断; - 渲染阶段禁止读写 DOM:DOM 操作放在 commit 生命周期或 Effects 中。
flowchart TB
classDef iA fill:#ffebee,stroke:#c62828,color:#b71c1c
classDef iB fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
A[渲染阶段 Render] --> B{是否产生副作用}
B -->|是 - 允许| C[收集到 Fiber 的 flags 上]
B -->|否 - 禁止| D{纯计算}
D -->|是| E[计算结果并入 props/state]
D -->|否| F[React StrictMode 双调用警告]
C --> G[提交阶段统一执行副作用]
G --> H[Passive Effects 异步执行]
G --> I[Layout Effects 同步执行]
click F "https://react.dev/reference/react/StrictMode" "StrictMode 双调用说明"
class A iB
class B iB
class C iB
class D iA
class E iB
class F iA
class G iB
class H iB
class I iB
终止与回滚
若渲染因高优先级任务而中断,且低优先级任务最终被废弃(比如用户又执行了更极端的操作),React 会直接丢弃该 workInProgress 树,不执行任何副作用,仅保留一条「已放弃」的记录。副作用只属于提交阶段这一原则,是 React 并发安全性的基石,也解释了为何官方反复强调「不要在渲染期间读写 DOM」。
七、总结
并发调度是一套「空间换时间、结构换可中断」的工程杰作:
- Fiber 链表树取代递归调用栈,让渲染可暂停、可恢复;
- Lane 位运算模型以常数级开销表达复杂的优先级集合运算;
- Scheduler + 时间切片保证输入响应优先,渲染让位于交互;
- 双缓存树让中断发生在幕后,用户永远看到完整的 UI;
- 副作用后置确保被丢弃的渲染不留任何痕迹。
对应用开发者而言,最重要的不是背诵源码细节,而是理解三条纪律:保持组件纯函数、副作用全部进 Effects、紧急更新用常规 setState、非紧急更新用 Transition。做到这三点,你的应用自然能在复杂交互下保持流畅。
最后给出进阶阅读建议:官方仓库的 react-reconciler/src/ReactFiberLane.js(Lane 定义)、scheduler/src/forks/Scheduler.js(调度循环)、ReactFiberWorkLoop.js(工作循环入口)。三份文件读完,并发调度再无秘密。
