专题知识学习:React Fiber
第一章:React Fiber 的诞生背景
1.1 React15 及之前版本的渲染瓶颈
递归不可中断的协调过程(stack reconciler栈协调器)
stack reconciler 是什么?
stack reconciler 是 React15 及之前版本中使用的虚拟 dom 协调算法,负责计算组件树的变化并更新真实 dom。其核心特点是基于递归遍历、不可中断的同步更新流程。
递归遍历虚拟 dom
采用深度优先遍历策略(FDS),从组件树根节点开始,递归调用组件的 render 方法,生成完整的虚拟 dom 树。递归遍历依赖于 JavaScript 调用栈(call stack),一旦开始就必须执行到底,无法中途暂停。
同步更新机制
所有更新(比如 setState)都会立即触发完整的 diff 计算,无法合并和延迟。当一个应用的组件树过于庞大时,一次递归 diff 计算是十分耗时的。stack reconciler 的工作流程分为两个阶段:
- 协调(reconciliation):该阶段中采用递归 diff 算法,对比新旧虚拟 dom 树的差异,找到需要进行更新的树节点,标记需要更新的节点的增删改
- 提交(commit):根据 diff 计算的结果,一次性同步执行所有的 dom 操作,并触发生命周期钩子(componentDidMount、componentDidUpdate等)
局限性
在了解上述知识后,我们可以很清晰的感知到 stack reconciler 的局限性:
- 无法中断长任务:递归遍历一旦开始就无法中断,这会占用 js 主线程,导致无法响应其他高优先级任务
- 缺乏任务优先级调度:所有的更新任务同步执行,享有同等优先级
- 内存泄漏风险:当组件树十分庞大时,深度递归可能会引起栈内存泄漏
大型应用中的掉帧问题和用户体验缺陷
“帧”是什么?
在计算机图形学与交互式应用中,帧是指屏幕画面的一次完整更新,它是衡量画面流畅度的核心单位,帧率(FPS)即是指每秒内渲染的帧数,人眼视觉对 60FPS 以上的变化感知有限,但低于 60FPS 时则会感知到延迟、卡顿。主流浏览器的帧率大多为 60FPS ,即每帧 16.67ms。
什么是掉帧,掉帧是怎么引起的?
掉帧即是指帧率未达预期,导致动画或交互卡顿。以浏览器举例,预期帧率 60FPS,则每一帧(16.67ms)的生命周期内,浏览器需要完成 js 执行、样式计算(style)、布局(layout)、绘制(paint)、合成(composite)等步骤,否则就会导致掉帧。
1.2 浏览器渲染机制与主线程阻塞
渲染机制
浏览器渲染过程是指浏览器将 HTML、CSS、JavaScript 等代码转换为用户可视界面的过程。浏览器通过多线程协作提高渲染效率,其中关键线程的分工如下:
| 线程 | 职责 | 示例场景 |
|---|---|---|
| 主线程(Main Thread) | 运行 JavaScript、DOM/CSS 解析、样式计算、布局、绘制、生成绘制指令 | setTimeout、React 渲染、事件处理 |
| 合成器线程(Compositor Thread) | 接收绘制指令、图层分块光栅化、加速图层合成、提交 GPU 显示 | 滚动、动画(如 transform 动画) |
| 光栅化线程(Raster Thread) | 将绘制指令转换为位图 | 处理图片解码、图层分块光栅化 |
正如上一章节所讲,浏览器渲染过程涉及多个阶段,且与主线程(Main Thread)紧密相关:
- 解析阶段:解析 HTML (遇到
<script>标签时会阻塞解析)和 CSS,生成 DOM 和 CSSOM - 样式计算阶段:将 DOM 和 CSSOM 合并,生成渲染树,树仅包含可见节点,并计算节点的最终样式
- 布局阶段:根据渲染树计算每个节点的精确位置和大小
- 绘制阶段:生成绘制指令,将布局结果转换为屏幕上的像素,输出绘制列表,记录绘制顺序
- 合成阶段:将页面分为多个图层并启用 GPU 加速合成
- 显示阶段:将合成后的位图通过显卡驱动传递给屏幕显示
浏览器的一次渲染过程即对应一帧的生成,单次渲染过程耗时过长即会导致帧率下降,即所谓的掉帧。结合浏览器关键线程分工职责和渲染过程的知识,可以知道,导致单次渲染耗时过长的原因有很多,与前端开发者息息相关的就是主线程阻塞。为了分析引起主线程阻塞的原因,我们还需要额外学习部分知识。
事件循环
JavaScript 的事件循环是什么?
JavaScript 的事件循环是其异步编程的核心机制,它决定了代码的执行顺序,使得单线程的 JavaScript 可以处理非阻塞任务
核心组成
事件循环的核心组成是调用栈和任务队列:
- 调用栈:按顺序执行同步代码,后进先出(LIFO),执行一个函数时,将其压入栈顶,函数返回后弹出。如果栈溢出(比如深度递归)会抛出异常。
- 任务队列:存储待执行的异步回调,分为宏任务队列、微任务队列、其他队列(比如requestAnimationFrame、web workers)。
调用栈与任务队列的协作模型的核心规则:
- 同步代码属于当前宏任务,直接由调用栈执行
- 异步任务分为宏任务、微任务两类,每次事件循环只执行一个宏任务,微任务必须在当前宏任务结束后立即执行,且必须清空队列
工作流程/执行顺序
- 执行当前调用栈中的同步代码(属于当前宏任务)
- 执行所有微任务,直到微任务队列为空
- 必要时渲染页面(浏览器决定)
- 从宏任务队列中取出一个任务执行(回到步骤 1)
核心规则
同步代码 > 微任务 > 渲染 > 宏任务
布局抖动(Layout Thrashing)
什么是布局抖动?
布局是指浏览器计算渲染树中每个节点的几何信息(精确位置、大小)的过程,发生在样式计算之后、绘制之前。修改影响几何属性的 CSS(如 width、margin)或读取布局属性(如 offsetWidth)都会触发布局重排。
布局抖动是指浏览器因频繁的强制同步布局造成的性能问题,表现为多次不必要的布局计算(重排/Reflow),影响页面渲染速度。其本质是代码中混合连续读写布局属性,迫使浏览器多次重新计算布局
常见触发场景
- 循环中读写布局属性
- 频繁访问布局 API:offsetTop、offsetLeft、scrollTop、getComputedStyle() 等
- 动画中混合读写
长任务(Long Task)
什么是长任务?
长任务是指主线程上连续执行时间超过 50ms 的 JavaScript 代码或渲染操作,它会阻塞用户交互和页面渲染,导致明显的卡顿
长任务的标准
- 时间阈值:50ms,是由 Google 根据人类感知延迟提出的临界值
- 检测工具:Chrome DevTools 的 Performance 面板中,长任务会被标记为红色区块并显示 Long Task 警告
常见的长任务场景
- 复杂的 JavaScript 计算
- 未优化的 dom 操作
- 同步网络请求
- 加载未优化的三方脚本
长任务优化手段
- 任务拆分
- web workers 多线程:将计算密集型任务转到worker
- 异步编程优化:通过 promise、async/await 等手段避免阻塞
- 虚拟列表优化渲染:仅渲染可视区域内容
- 惰性加载:按需加载非关键脚本
主线程阻塞原因分析
现在,我们可以知道,引起主线程阻塞的主要原因有以下几点:
- 布局抖动
- 长任务
- 过多的微任务
1.3 现代前端应用的需求演进
动画/手势的高优先级更新
在现代前端应用中,动画/手势的高优先级更新是提升用户体验(UX)和界面流畅性的关键设计,其意义有以下几个方面:
- 确保交互即时响应
- 避免掉帧现象的发生
- 提升手势操作的跟手性
- 支持复杂的 UI 设计:拖拽、捏合缩放、页面过渡动画等复杂交互依赖高优先级更新
- 与浏览器渲染管线的协同优化:高优先级动画(如 transform)可由合成器线程直接处理,无需主线程参与,从而避免布局/重绘
通过优先级调度策略,可以提升用户留存率和满意度。
异步数据加载与Suspense的诉求
传统前端应用中,通常会遇到以下几大问题:
- “白屏”等待:传统应用在数据加载完全前,页面通常显示空白或 loading 图标,用户无法感知进度。
- 不必要的加载状态:传统模式下,各个组件独立维护 loading 状态,会导致多次闪烁的 loading 提示
- 竞态条件问题:典型场景是快速切换标签页时,前一次请求可能覆盖后一次请求的结果,比如从详情 A 跳到详情 B
而现代前端应用中,这些问题是严重影响用户体验的,针对以上问题,衍生出了以下现代需求:
- 骨架屏:在数据加载时显示占位 UI,提升感知速度
- 流式渲染:逐步发送 HTML 片段,让用户尽早看到内容
- 状态统一:统一管理数据状态,仅在所有数据就绪后一次性渲染
- AbortController:可中断正在进行的异步操作,避免资源浪费和竞态条件
这就要求应用在数据管理方案上能支持异步数据管理,因此 React 团队提出了Suspense 声明式异步数据管理方案。
为了解决 React15 及之前版本的渲染问题,应对日益激增的现代前端应用需求,React Fiber 架构于 16.x 版本诞生,并于后续版本中逐步优化完善。
第二章:Fiber架构的核心设计思想
2.1 Fiber 的三大角色定义
作为数据结构的Fiber节点(链表节点)
每个 Fiber 节点是一个 JavaScript 对象,React 源码中对 Fiber 节点的定义如下:
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber 链表指针
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
// 工作进度
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects 副作用标记
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 优先级调度
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 双缓存树指针
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
if (__DEV__) {
// This isn't directly used but is handy for debugging internals:
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
作为执行单元的任务分片(Unit of Work)
在第一章节中,我们了解了长任务的相关知识,知道长任务会阻塞主线程,引起掉帧问题。为了解决这个问题,我们需要将长任务进行拆分,并在浏览器的每一帧中限制主线程执行任务的时间(React 默认初始时间是 5ms),这种将长任务拆成多段微小任务的技术被称为任务分片,分配到每一帧中去执行的操作被称为时间切片,二者协同实现 React 的高效渲染。
实现任务分片的关键是将同步更新变为可中断的异步更新:
- 构建链表树:通过 Fiber 节点的链表结构构建链表树,替代递归调用栈
- 分片执行:React 通过循环逐个处理 Fiber 节点,每处理完一个节点后检查剩余时间,决定继续还是暂停
- 中断和恢复:当有高优先级任务(如用户点击)或当前分片时间用尽时暂停任务,通过保存当前处理的 Fiber 节点指针,下次从断点继续
作为调度单位的优先级载体(Lane模型)
React Fiber 架构中实现任务优先级调度的核心思想是:将优先级信息分散在 Fiber 树的各个节点和更新信息中,通过动态计算(lanes)和调度器(scheduler)的协作实现优先级调度。
Lane(车道)的定义
- 每个 Lane 是一个32位的二进制位
- 每个二进制位代表一种优先级类型
- 数字越小(位越低)优先级越高
- Lane 互斥
Lane 的类型
React 18 版本中定义的类型有以下几种:
// 优先级从高到低
export const SyncLane: Lane = 0b0000000000000000000000000000001; // 同步任务(最高)
export const InputContinuousLane: Lane = 0b0000000000000000000000000000100; // 用户输入
export const DefaultLane: Lane = 0b0000000000000000000000000100000; // 默认更新
export const TransitionLane: Lane = 0b0000000000000000001000000000000; // 过渡更新,比如useTransition
export const IdleLane: Lane = 0b0100000000000000000000000000000; // 空闲任务(最低)
Lane 集合(Lanes)
多个 Lane 可以组合:lanes = InputLane | DefaultLane,表示一组需要处理的优先级集合
Lane 模型的工作原理
- 优先级分配:更新阶段根据交互类型分配 Lane
- 优先级收集:每个 Fiber 节点对象都有
lanes属性和childLanes属性,lanes 属性保存该节点需要处理的优先级合集,childLanes 属性保存子节点中所有未处理的优先级合集 - 优先级调度:Fiber 树根节点合并所有子节点的 lanes 和 childLanes -> 从合并后的优先级合集中选取最高优先级的 Lane 进行处理 -> 只处理与选定 Lane 匹配的更新
这里先只做简单的概念了解,详细知识将在 4.1 章节中进行讲解。
2.2 关键设计目标
我们已经知道在 React15 及之前版本中,渲染一旦开始便无法中断,并且所有任务同步进行,享有同等优先级,会导致交互延迟。Fiber 架构为了解决这些问题的核心设计目标有三个
可中断、可恢复的渲染流程
关键设计
要实现可中断、可恢复的渲染流程,关键设计有三点:
- 链表结构替代递归结构:Fiber 通过
child、sibling、return构成树形链表 - 全局指针跟踪进度:
nextUnitOfWork记录当前处理节点 - 双缓存机制:
current tree和work-in-progress tree两棵树交替更新
完整流程
graph TD
A[触发更新] --> B{有高优先级任务}
B -->|是| C[中断当前任务]
B -->|否| D[开始Render阶段]
D --> E[处理Fiber节点]
E --> F{时间用完}
F -->|是| C
F -->|否| G{还有子节点}
G -->|是| H[移动到子节点]
G -->|否| I{还有兄弟节点}
I -->|是| J[移动到兄弟节点]
I -->|否| K[回溯到父节点]
K --> L{根节点}
L -->|否| E
L -->|是| M[完成Render阶段]
M --> N[Commit阶段]
N --> O[更新DOM]
C --> P[保存进度]
P --> Q[执行高优先级任务]
Q --> R[恢复低优先级任务]
实现原理
- 渲染阶段可中断,核心代码如下:
// 主工作循环 /** @noinline */ function workLoopConcurrentByScheduler() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { // $FlowFixMe[incompatible-call] flow doesn't know that shouldYield() is side-effect free performUnitOfWork(workInProgress); } } // 检查是否需要中断 function shouldYield(): boolean { if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) { // Yield now. return true; } const timeElapsed = getCurrentTime() - startTime; if (timeElapsed < frameInterval) { // The main thread has only been blocked for a really short amount of time; // smaller than a single frame. Don't yield yet. return false; } // Yield now. return true; } - 进度保存和恢复:中断时保留
nextUnitOfWork指针指向未完成的 Fiber 节点,恢复时从上次中断的节点继续遍历,采用深度优先遍历策略,优先处理子节点(child),无子节点时处理兄弟节点(sibling),无兄弟节点时回溯父节点(return) - 双缓存保证一致性:current tree 展示当前界面,中断时保持不变,work-in-progress tree 构建新的 Fiber 树,构建完成后一次性替换 current tree 变成新的 current tree
React Fiber 架构通过可中断、可恢复的渲染流程拥有类似于操作系统的“多任务处理”能力,最终实现渲染不阻塞交互的用户体验
时间切片(Time Slicing)与增量渲染
在 React Fiber 中,时间切片和增量渲染是两个紧密相关的核心概念,共同解决了渲染过程中主线程阻塞的问题,但他们的关注点不同
时间切片 - 时间维度的解决方案
时间切片是将连续长任务切割成多个微小任务,并分散在浏览器的多个渲染帧中去执行的技术
它的核心目标有 2 个:
- 防止 JavaScript 代码执行时间超过 50ms
- 保证浏览器的每一帧中有足够的时间去执行渲染、动画和交互
其核心实现原理是通过 requestIdleCallback(React17及以下) 或 MessageChannel(React18) 来检测剩余时间,当时间用尽时中断任务并保存状态,下次空余时间到来时从中断点恢复
增量渲染 - 任务维度的解决方案
增量渲染是将整个渲染过程分解为多个可独立执行的子任务,并按优先级逐步完成的技术
它的核心目标是:
- 将大型渲染任务分解为可独立执行的小任务
- 允许先呈现部分内容,再逐步补充剩余内容
其实现原理是通过任务分片将组件树拆分为 Fiber 节点链表,通过 Lane 模型区分任务优先级,完成的部分先提交呈现,并结合双缓存机制保证渲染过程不影响当前显示:
graph LR
A[开始渲染] --> B[处理根节点]
B --> C[处理子节点1]
C --> D[处理子节点1.1]
D --> E[提交已完成部分]
E --> F[处理兄弟节点1.2]
F --> G{时间用尽}
G -->|是| H[保存状态并暂停]
G -->|否| I[继续处理]
时间切片和增量渲染协同工作流程如下:
- 增量渲染拆解任务:将整个渲染任务拆分为 Fiber 节点任务队列
- 时间切片分配时间:将浏览器每个渲染帧的空闲时间分配给任务队列
- 优先级调度介入:允许高优先级任务抢占当前执行
- 渐进式提交:完成的部分内容可先提交显示
基于优先级的任务调度
React 使用 Lane 模型定义优先级,每个优先级对应一个二进制位
调度流程
- 任务标记阶段:当触发更新时,React 根据场景分配优先级
function requestUpdateLane() { if (isTransition) return TransitionLane; // useTransition 更新 if (isUserBlockingEvent) return InputLane; // 用户交互事件 return DefaultLane; // 默认更新 } // 示例:点击事件触发的更新 button.addEventListener('click', (event) => { const updateLane = getEventPriority(event); // 返回 InputContinuousLane scheduleUpdate(fiber, updateLane); }); - 任务调度阶段:根据优先级和剩余时间控制执行顺序
sequenceDiagram participant Browser as 浏览器 participant Scheduler as React调度器 participant Renderer as 渲染器 Browser->>Scheduler: 新帧开始(16.6ms) Scheduler->>Renderer: 获取待处理任务 Renderer->>Renderer: 按优先级排序任务 loop 时间切片执行 Renderer->>Renderer: 执行最高优先级任务 Renderer-->>Scheduler: 检查剩余时间 Scheduler->>Browser: 时间用尽则归还控制 end Browser->>Browser: 执行绘制/用户输入 Browser->>Scheduler: 下一帧继续 - 中断与抢占机制:
- 高优先级任务可直接中断正在执行的低优先级任务
- 被中断的任务可保存进度到 nextUnitOfWork 指针中,后续恢复
- 饥饿问题处理:长时间未执行的低优先级任务会被逐步提升优先级
关键调度策略
- 批量更新合并:比如相同优先级的多个 setState 会被合并
- 优先级反转预防:
function ensureRootIsScheduled(root) { const nextLanes = getNextLanes(root, root.pendingLanes); const highestPriorityLane = getHighestPriorityLane(nextLanes); // 当前执行中的低优先级任务被高优先级打断 if (existingCallbackPriority !== highestPriorityLane) { cancelCallback(existingCallbackNode); // 取消旧任务 scheduleNewCallback(highestPriorityLane); // 调度新任务 } } - 时间切片分配:不同优先级的任务获得的时间片不同
优先级 单次分配时间 是否可中断 SyncLane 不限 否 InputContinuousLane 5ms 是(仅限更高优先级) DefaultLane 2ms 是 TransitionLane 1ms 是
第三章:Fiber的数据结构与算法实现
3.1 Fiber节点的详细结构解析
child、sibling、return指针的链表关系
Fiber 架构的设计哲学在于将树形结构转化为单向链表+树形回溯的混合结构,在保持父子关系的同时获得链表的高效遍历能力。实现该混合结构的关键点就是 child、sibling、return 这三个指针。
三个指针的作用与关系
| 指针名 | 指向 | 功能描述 | 类比传统树结构 |
|---|---|---|---|
| child | 第一个子节点 | 向下遍历的入口 | node.firstChild |
| sibling | 下一个兄弟节点 | 横向遍历同级节点 | node.nextSibling |
| return | 父节点 | 完成当前分支后向上回溯 | node.parentNode |
链表结构图解
假设有如下组件树:
<App>
<Header />
<Content>
<Sidebar />
<Main />
</Content>
</App>
那该组件树对应的 Fiber 链表结构如下:
graph TD
A[App] -->|child| B[Header]
B -->|sibling| C[Content]
C -->|child| D[Sidebar]
D -->|sibling| E[Main]
E -->|return| C
D -->|return| C
C -->|return| A
B -->|return| A
指针关系表如下:
| Fiber 节点 | child | sibling | return |
|---|---|---|---|
| App | Header | null | null |
| Header | null | Content | App |
| Content | Sidebar | null | App |
| Sidebar | null | Main | Content |
| Main | null | null | Content |
alternate与双缓存树(Current/WorkInProgress)
alternate 指针和双缓存树是实现并发渲染和状态一致性的核心机制
双缓存树的本质
基本概念
| 树类型 | 作用 | 特点 |
|---|---|---|
| current | 当前界面正在显示内容对应的 Fiber 树 | 用户正在交互的稳定版本 |
| workInProgress | 正在内存中构建的 Fiber 树(即将成为下一针显示内容) | 可中断、可丢弃的中间状态 |
alternate 指针的作用:每个 Fiber 节点都有一个 alternate 字段,指向另一棵树上的对应节点(current fiberNodeA.alternate <-> workInProgress fiberNodeA.alternate)
双缓存树的工作流程
初始渲染阶段:
sequenceDiagram
participant R as React
participant D as DOM
R->>R: 创建 WorkInProgress 树(初始为空)
R->>R: 从 Root 开始构建 Fiber 节点
R->>R: 每个新节点设置 alternate=null
R->>D: 首次渲染完成后, WorkInProgress 树变为 Current 树
更新阶段:
sequenceDiagram
participant C as Current 树
participant W as WorkInProgress 树
participant R as React 调度器
R->>W: 从 Current 树的 Root 克隆节点
C->>W: 通过 alternate 互相指向
loop 渲染阶段
R->>W: 增量构建/更新节点
W->>C: 通过 alternate 对比差异
end
R->>C: 提交完成后交换两棵树
effectTag与副作用链表(Effect List)
在了解 effectTag 之前,我们先了解下什么是 side effects(副作用)。
在 React 中,副作用(Side Effects)是指那些在组件渲染过程中无法完成的操作,因为它们会影响到组件外部或渲染周期之外的事物。简单来说,任何与 React 的纯渲染逻辑无关的操作都属于副作用。
为什么需要管理副作用?
- React 组件应该是纯函数(在相同输入下返回相同输出),但副作用打破了这种纯度。因此,React 提供了专门的机制来处理副作用,以确保它们不会干扰组件的渲染过程,同时避免内存泄漏和不一致。
副作用的执行时机:
- React 将副作用延迟到浏览器完成绘制之后执行,以避免阻塞屏幕更新。因此,副作用函数在组件渲染完成后运行(相当于类组件的
componentDidMount和componentDidUpdate)。
React 管理副作用的钩子:useEffect、useLayoutEffect或自定义 Hooks
副作用的本质特征
| 特征 | 说明 | 示例 |
|---|---|---|
| 非纯操作 | 违反纯函数原则(相同输入 ≠ 相同输出) | 数据获取、DOM 手动操作 |
| 外部依赖 | 与 React 渲染流程外的系统交互 | 访问浏览器 API、网络请求 |
| 时序敏感性 | 执行时机影响结果 | 事件监听、定时器 |
| 资源管理 | 需要显式清理 | 取消订阅、移除事件监听 |
副作用分类机制
| 副作用类型 | 处理方式 | 对应 Hook |
|---|---|---|
| 同步 DOM 副作用 | 布局阶段同步执行 | useLayoutEffect |
| 异步副作用 | 浏览器绘制后执行 | useEffect |
| 状态更新副作用 | 随渲染流程处理 | useState/useReducer setter |
| 回调副作用 | 事件处理中执行 | 事件处理函数 |
正确处理副作用的规则
- 副作用隔离原则:避免渲染中执行(不要在 render 函数中或函数式组件主题中直接操作副作用),使用 Hook 封装(比如 useEffect)
- 明确声明依赖项(useEffect dependencies)
- 副作用清理机制(useEffect return)
effectTag 的本质与作用
- effectTag是副作用标记系统。
- 二进制位掩码:每个 effectTag 是一个二进制数,表示需要执行的副作用类型
- 高效内存管理:通过位运算组合多个标记(如 Placement | Update)
- 精确追踪:标记哪些 Fiber 节点需要进行 DOM 操作或其他副作用
常见的 effectTag 值
| 标记 | 值(二进制) | 对应操作 |
|---|---|---|
| NoEffect | 0b00000000 | 无副作用 |
| Placement | 0b00000010 | 插入新节点 |
| Update | 0b00000100 | 更新属性/样式 |
| Deletion | 0b00001000 | 删除节点 |
| Snapshot | 0b00010000 | 生命周期 getSnapshotBeforeUpdate |
| Passive | 0b00100000 | useEffect 的副作用 |
| Callback | 0b01000000 | setState 的回调 |
副作用链表(effect list)是只包含有副作用的 Fiber 节点的链表结构,它的构建时机是在 completeWork 阶段串联有 effectTag 的节点。它的关键指针如下:
- firstEffect:链表头节点
- nextEffect:下一个待处理节点
- lastEffect:链表尾节点
副作用链表的构建过程
function completeWork(fiber) {
// 处理当前节点工作...
// 构建 Effect List
if (fiber.effectTag > NoEffect) {
if (fiber.return !== null) {
// 将当前节点添加到父节点的 Effect List
if (fiber.return.firstEffect === null) {
fiber.return.firstEffect = fiber;
} else {
fiber.return.lastEffect.nextEffect = fiber;
}
fiber.return.lastEffect = fiber;
}
}
}
effectTag/副作用链表的工作原理
- 标记阶段:在 beginWork 中标记需要执行的 DOM 操作
- 链表构建:在 completeWork 阶段串联具有 effectTag 的节点
- 提交阶段:按链表顺序执行 DOM 操作
3.2 深度优先遍历的迭代实现
递归遍历的问题与链表遍历的优势
在 React15 及之前版本的 stack reconciler 协调器采用的遍历策略是递归遍历
递归遍历的问题
- 无条件全量遍历:从根节点开始递归处理每一个节点,无论节点状态是否变化
- 不可中断:依赖于 JavaScript 栈调用,一旦开始就必须执行到底,中途无法中断或跳过子节点
- 性能缺陷:阻塞主线程、无优先级调度
链表遍历的优势
- 可中断/恢复:通过全局变量(nextUnitOfWork)保存进度,结合 requestIdleCallback 或 messageChannel 时间切片实现中断和恢复。
- 按需遍历:通过 lanes 和 childLanes 标记 Fiber 节点优先级,跳过无需更新的子树,时间复杂度从递归的 O(n) 优化到 O(m)
- 优先级调度:结合 Lane 模型动态调整遍历顺序,实现高优先级任务中断低优先级任务
- 内存安全:链表遍历在堆内存中进行,不受调用栈限制,没有递归栈帧累积,因此支持任意深度的组件树
performUnitOfWork源码解析(含配图)
我们首先了解下 Fiber 架构中的工作循环:
/** @noinline */
function workLoopConcurrentByScheduler() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// $FlowFixMe[incompatible-call] flow doesn't know that shouldYield() is side-effect free
performUnitOfWork(workInProgress);
}
}
performUnitOfWork 是 Fiber 架构中工作循环(workLoop)的最小执行单元,源码位置见这里
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
if (__DEV__) {
next = runWithFiberInDEV(
unitOfWork,
beginWork,
current,
unitOfWork,
entangledRenderLanes,
);
} else {
next = beginWork(current, unitOfWork, entangledRenderLanes);
}
stopProfilerTimerIfRunningAndRecordDuration(unitOfWork);
} else {
if (__DEV__) {
next = runWithFiberInDEV(
unitOfWork,
beginWork,
current,
unitOfWork,
entangledRenderLanes,
);
} else {
next = beginWork(current, unitOfWork, entangledRenderLanes);
}
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
其核心代码如下,主要流程分为两个阶段:
function performUnitOfWork(unitOfWork: Fiber): void {
// 1. 开始工作阶段(递)
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork, entangledRenderLanes);
// 2. 完成工作阶段(归)
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
beginWork - 递阶段:处理组件更新(props/state 计算、diff 等)
核心代码逻辑:
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 检查是否需要跳过更新(优化手段)
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps === newProps && !hasLegacyContextChanged()) {
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
}
}
// 根据组件类型执行不同处理
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, ...);
case ClassComponent:
return updateClassComponent(current, workInProgress, ...);
case HostComponent: // DOM 节点
return updateHostComponent(current, workInProgress, ...);
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
// ...其他类型处理
}
}
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
核心操作:
- 优先级过滤:通过 renderLanes 跳过低优先级任务
- diff 算法:在 updateFunctionComponent -> reconcileChildren 中生成子 Fiber
- 副作用标记:设置 effectTag
completeUnitOfWork - 归阶段:完成 DOM 准备、收集副作用
核心代码逻辑:
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork: Fiber = unitOfWork;
do {
if ((completedWork.flags & Incomplete) !== NoFlags) {
// This fiber did not complete, because one of its children did not
// complete. Switch to unwinding the stack instead of completing it.
//
// The reason "unwind" and "complete" is interleaved is because when
// something suspends, we continue rendering the siblings even though
// they will be replaced by a fallback.
const skipSiblings = workInProgressRootDidSkipSuspendedSiblings;
unwindUnitOfWork(completedWork, skipSiblings);
return;
}
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next;
startProfilerTimer(completedWork);
if (__DEV__) {
next = runWithFiberInDEV(
completedWork,
completeWork,
current,
completedWork,
entangledRenderLanes,
);
} else {
next = completeWork(current, completedWork, entangledRenderLanes);
}
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordIncompleteDuration(completedWork);
}
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
// $FlowFixMe[incompatible-type] we bail out when we get a null
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
核心操作:
- DOM 准备:在 completeWork 中创建/更新 DOM
- 副作用收集:通过 firstEffect/lastEffect构建线性副作用链表,供提交阶段批量处理
- 回溯机制:通过 return 指针实现非递归遍历
遍历过程图解
还是以之前的组件树为例:
<App>
<Header />
<Content>
<Sidebar />
<Main />
</Content>
</App>
其链表遍历顺序图:
graph LR
A[App] --> B[Header]
B --> C[Content]
C --> D[Sidebar]
D --> E[Main]
E --> F[回溯Content]
F --> G[回溯App]
具体步骤:
- performUnitOfWork(App) → beginWork(App) → 返回子节点 Header → workInProgress = Header
- workLoopSync → performUnitOfWork(Header) → beginWork(Header) → 无子节点,返回null
- completeUnitOfWork(Header) → workInProgress = Header.siblingFiber(即Content)
- performUnitOfWork(Content) → beginWork(Content) → 返回子节点 Sidebar → workInProgress = Sidebar
- workLoopSync → performUnitOfWork(Sidebar) → beginWork(Sidebar) → 无子节点,返回null
- completeUnitOfWork(Sidebar) → workInProgress = Sidebar.siblingFiber(即Main)
- performUnitOfWork(Main) → beginWork(Main) → 无子节点无兄弟节点,返回 null
- completeUnitOfWork(Main) → 无子节点,无兄弟节点 → return 回溯到 Content → 无兄弟节点 → completeUnitOfWork(Content) → return 回溯到 App → 遍历结束
performUnitOfWork 的完整工作流程
sequenceDiagram
participant Scheduler
participant performUnitOfWork
participant beginWork
participant completeUnitOfWork
Scheduler->>performUnitOfWork: 分配任务(Fiber节点)
performUnitOfWork->>beginWork: 处理当前节点
alt 有子节点
beginWork-->>performUnitOfWork: 返回子节点
else 无子节点
beginWork-->>performUnitOfWork: null
performUnitOfWork->>completeUnitOfWork: 进入完成阶段
completeUnitOfWork-->>performUnitOfWork: 返回兄弟/父节点
end
performUnitOfWork-->>Scheduler: 返回下一个节点
3.3 Diff算法的Fiber化改造
同级比较(Key优化)在Fiber中的实现
React 列表元素为什么要加上 key 属性?
无论是传统 Diff 还是 Fiber Diff,key 的核心作用都是唯一标识同级元素。传统虚拟 DOM Diff 在 key 的利用上有两个瓶颈:
双指针遍历算法(O(n²) 复杂度)
传统 Diff 通过两层循环匹配新旧节点:
for (let i =0; i < newChildren.length; i++) {
for (let j =0; j < oldChildren.length; j++) {
if (newChild[i].key === oldChild[j].key) {
// 匹配成功,复用节点
break;
}
}
}
这带来的问题是:
- 长度为 n 的列表最坏情况下需要 n² 次比较,性能随列表长度增大急剧下降
- 仅能通过位置索引猜测节点移动,容易产生冗余 DOM 操作
复用粒度有限
- 仅能复用虚拟 DOM 节点对象,仍需执行组件的生命周期和 DOM 属性对比
- 无法跳过状态未发生改变的组件渲染(比如 shouldComponentUpdate 需手动优化)
Fiber 架构对比传统虚拟 DOM Diff 在 key 的利用上实现了质的飞跃。
在 Fiber 架构中,同级比较的核心逻辑集中在 reconcileChildren 函数中,它负责对比新旧子节点并生成新的 Fiber 树。beginWork 函数根据组件类型执行对应处理时触发该函数。
入口函数:reconcileChildren
根据当前是首次渲染还是更新调用 mountChildFibers 或 reconcileChildFibers
- mountChildFibers:专用于组件首次渲染(mount 阶段)时创建子 Fiber 节点的方法,其核心特点是通过跳过副作用标记和 Diff 算法提升性能
- reconcileChildFibers:更新阶段处理子节点协调的核心逻辑,根据子节点类型做不同处理,比如子节点为数组时调用
reconcileChildrenArray函数处理。
核心优化逻辑
reconcileChildrenArray 采用多阶段遍历 + key 映射表的策略,将时间复杂度优化至O(n),主要优化包括:
- 两轮遍历:先尝试从左到右顺序匹配,再处理移动/新增/删除
- key 映射:剩余未匹配的旧节点存入map,实现O(1)查找
- 节点复用:通过 key 和 type 精确匹配可复用的 Fiber 节点
- 最小化 DOM 操作:仅标记需要移动和删除的节点
源码解析:以 reconcileChildrenArray 为例
阶段一:顺序遍历匹配(从左到右)
let oldFiber = currentFirstChild; // 旧 Fiber 链表头节点
let lastPlacedIndex = 0; // 记录最后一个无需移动的节点索引
let newIdx = 0;
let nextOldFiber = null;
// 第一轮遍历:顺序匹配 key 相同的节点
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 尝试复用旧 Fiber(key & type 匹配)
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
// key 不匹配,跳出循环
break;
}
// 标记是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
}
优化点:
- updateSlot:检查 key 和 type,匹配则复用旧 Fiber,否则返回null
- placeChild:比较旧 Fiber 的 index 和 newIdx,决定是否标记 placement(移动)
阶段二:处理剩余节点
情况一:新节点已遍历完
if (newIdx === newChildren.length) { // 删除剩余旧节点 deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; }- 优化点:直接删除未匹配的旧节点,无需进一步比较
情况二:旧节点已遍历完
if (oldFiber === null) { // 创建剩余新节点 for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild(returnFiber, newChildren[newIdx], lanes); // 链接到链表 } return resultingFirstChild; }- 优化点:剩余新节点直接创建,无需匹配
情况三:新旧节点均未遍历完
// 构建旧节点 Map<key|index, Fiber> const existingChildren = mapRemainingChildren(oldFiber); // 遍历剩余新节点,尝试从 Map 中匹配 for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, ); if (newFiber !== null) { // 标记移动或复用 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); } } // 删除未匹配的旧节点 existingChildren.forEach(child => deleteChild(returnFiber, child));- 优化点:
- mapRemainingChildren:将剩余旧节点存入 map,key 优先,无 key 时使用 index
- updateFromMap:从 map 中查找可复用节点,减少遍历次数
- placeChild:仅对需要移动的节点标记 placement,减少不必要的 DOM 操作
- 优化点:
key的核心作用
- 精确匹配:key 是 React 中节点的唯一标识,应避免复用
- 高效复用:通过 key 直接定位旧节点,而非递归比较
- 移动优化:key 相同的节点即使位置发生改变,也能被正确标记移动,而非删除重建
性能对比(传统虚拟 DOM Diff 对比 Fiber Diff)
| 场景 | 传统 Diff | Fiber Diff |
|---|---|---|
| 时间复杂度 | O(n²)(双指针循环) | O(n) (map查找) |
| DOM 操作 | 可能多次移动/重建 | 仅进行必要更新(placement/deletion) |
| 状态保留 | 仅复用 DOM | 组件状态、hooks、ref全保留 |
| 列表渲染 | 性能随列表长度下降明显 | 万级列表仍然流畅 |
节点复用策略与bailout机制
复用条件:key 和 type 双匹配
源码入口:reconcileChildFibers → reconcileChildrenArray / updateSlot
核心代码逻辑(简化自ReactFiberChild.js):
function updateSlot(returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, lanes: Lanes): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
const key = oldFiber !== null ? oldFiber.key : null;
if (typeof newChild === 'object' && newChild !== null) {
if (newChild.key === key) {
if (newChild.type === oldFiber?.type) {
// 复用旧 Fiber(保留 state/DOM/hooks)
const existing = useFiber(oldFiber, newChild.props);
coerceRef(existing, element);
existing.return = returnFiber;
return existing;
}
// key 匹配但 type 不匹配,销毁旧节点
deleteChild(returnFiber, oldFiber);
} else {
return null
}
}
return null; // 不匹配则创建新节点
}
复用规则
- key 和 type 都相同:调用 useFiber 克隆旧节点,保留:
- 组件实例(class组件)和 hooks 链表(函数式组件)
- DOM 引用(避免重建)
- 状态和refs
- key 相同,type 不同:销毁旧节点(标记deletion),创建新节点
- key 不同:直接创建新节点
useFiber实现(状态保留核心)
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0; // 重置索引
clone.sibling = null; // 断开兄弟链接
return clone;
}
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double buffering pooling technique because we know that we'll
// only ever need at most two versions of a tree. We pool the "other" unused
// node that we're free to reuse. This is lazily created to avoid allocating
// extra objects for things that are never updated. It also allow us to
// reclaim the extra memory if needed.
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// 省略部分源码
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
// Needed because Blocks store data on type.
workInProgress.type = current.type;
// We already have an alternate.
// Reset the effect tag.
workInProgress.flags = NoFlags;
// The effects are no longer valid.
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = -0;
workInProgress.actualStartTime = -1.1;
}
}
// Reset all effects except static ones.
// Static effects are not specific to a render.
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
// 省略部分源码
return workInProgress;
}
关键点:
- 通过 alternate 指针复用旧 Fiber 对象(双缓冲技术)
- 保留 stateNode(DOM/实例)和 memoizedState(状态)
bailout 机制(跳过子树协调)核心逻辑
源码入口:beginWork → updateFunctionComponent / updateClassComponent
核心代码逻辑(精简自ReactFiberBeginWork.js):
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps && // Props 浅比较
!hasLegacyContextChanged() && // 旧 Context 未变
workInProgress.type === current.type && // 组件类型相同
!includesSomeLane(renderLanes, current.lanes) // 无高优先级更新
) {
didReceiveUpdate = false
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
bailout 条件
- props 浅层相等(Object.is比较)
- context 值未变化(新旧 Provider 值浅比较)。
- 组件类型相同(无 type 变化)。
- 无更高优先级更新(通过 lanes 模型判断)
bailoutOnAlreadyFinishedWork 实现
function bailoutOnAlreadyFinishedWork(current: Fiber, workInProgress: Fiber, renderLanes: Lanes): Fiber | null {
cloneChildFibers(current, workInProgress); // 克隆子树
return workInProgress.child;
}
关键操作:
- 子树克隆:直接拷贝 current 树的子链表到 workInProgress 树。
- 副作用继承:保留旧 Fiber 的 subtreeFlags(标记子树是否需要更新)。
性能优化对比
| 场景 | 传统 Diff | Fiber Diff |
|---|---|---|
| 节点复用 | 仅复用 DOM | 状态+DOM+hooks |
| 条件判断 | 无系统化 bailout | 多维度检测(props/context/优先级) |
| 子树跳过 | 不可能 | 直接克隆整棵子树(O(1) 操作) |
| 时间复杂度 | O(n²)(递归) | O(n)(map查找+链表遍历) |
第四章:Fiber的调度系统与并发模式
4.1 调度器(Scheduler)的实现原理
调度器(scheduler)是 Fiber 架构中并发模式的核心实现。调度器的核心目标有三点:
- 任务可中断与恢复
- 优先级调度
- 时间切片
React17 及之前版本中任务中断与时间切片的核心逻辑在于浏览器 requestIdleCallback API 的利用,但该 API 有几个问题:
- 不可靠的执行时机:requestIdleCallback 的执行依赖于浏览器的空闲时间,但不同浏览器的实现差异较大,且可能被扩展插件、防病毒软件等干扰
- 无法保证任务顺序:requestIdleCallback 的回调执行顺序可能被打乱(尤其是设置了 timeout 时),而 React 需要精确控制任务优先级
- 兼容性问题:部分浏览器(如旧版 Safari)不支持 requestIdleCallback,或实现不一致
- 时间切片需求:React 需要将任务拆分为 5ms 左右的小块,而 requestIdleCallback 无法提供这种精细控制
React 18 改用 MessageChannel 模拟 requestIdleCallback 的行为,并在此基础上实现更高级的调度策略
MessageChannel与时间切片
MessageChannel 是浏览器提供的用于跨文档通信(跨窗口/iframe)的 API,也被广泛用于主线程的任务调度
基本用法
MessageChannel 创建一个双向通信通道,包含两个MessagePort:
- port1:发送消息
- port2:接收消息
const channel = new MessageChannel(); const { port1, port2 } = channel; // port2 监听消息 port2.onmessage = (event) => { console.log("Received:", event.data); }; // port1 发送消息 port1.postMessage("Hello");
特点
- 异步执行:postMessage 是宏任务,类似于 setTimeout(fn, 0),但比setTimeout更高效
- 零延迟:浏览器会尽快执行回调,不受事件循环延迟影响
- 跨线程通信:可用于 Web Worker、Service Worker 等场景(但 React 调度器仅用于主线程)
React scheduler 如何使用 MessageChannel
关键代码(Scheduler.js):
const performWorkUntilDeadline = () => {
if (enableRequestPaint) {
needsPaint = false;
}
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
// remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
// workLoop 循环执行任务直到时间片用尽
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
};
function flushWork(initialTime: number) {
// 省略非关键代码
try {
if (enableProfiling) {
try {
return workLoop(initialTime);
} catch (error) {
// 省略非关键代码
}
} else {
// No catch in prod code path.
return workLoop(initialTime);
}
} finally {
// 省略非关键代码
}
}
function workLoop(initialTime: number) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (!enableAlwaysYieldScheduler) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
// 省略
} else {
pop(taskQueue)
}
// 省略任务队列循环处理逻辑代码
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
function handleTimeout(currentTime: number) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback();
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline; // 接收消息后执行任务
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null); // 触发异步执行
};
调度流程如下:
- 任务入队:unstable_scheduleCallback 将任务加入最小堆
- 触发调度:requestHostCallback 通过 port.postMessage(null) 请求调度
- 执行任务:浏览器在下一事件循环中调用 port1.onmessage,执行 flushWork
- 时间切片:flushWork 每次执行最多 5ms,超时则暂停(shouldYieldToHost)
优先级标记(Lane模型)与任务队列
之前章节我们已经学习过优先级载体 Lane 模型的概念,其对应代码定义如下:
// SchedulerPriorities.js
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
优先级与超时时间映射
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
// Times out immediately
timeout = -1; // 同步执行
break;
case UserBlockingPriority:
// Eventually times out
timeout = userBlockingPriorityTimeout; // 250ms 超时
break;
case IdlePriority:
// Never times out
timeout = maxSigned31BitInt; // 最大超时,var maxSigned31BitInt = 1073741823; 约12天
break;
case LowPriority:
// Eventually times out
timeout = lowPriorityTimeout; // 10s 超时
break;
case NormalPriority:
default:
// Eventually times out
timeout = normalPriorityTimeout; // 5s 超时
break;
}
任务队列(最小堆)
// SchedulerMinHeap.js
type Heap<T: Node> = Array<T>;
type Node = {
id: number,
sortIndex: number,
...
};
export function push<T: Node>(heap: Heap<T>, node: T): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
export function peek<T: Node>(heap: Heap<T>): T | null {
return heap.length === 0 ? null : heap[0];
}
export function pop<T: Node>(heap: Heap<T>): T | null {
if (heap.length === 0) {
return null;
}
const first = heap[0];
const last = heap.pop();
if (last !== first) {
// $FlowFixMe[incompatible-type]
heap[0] = last;
// $FlowFixMe[incompatible-call]
siftDown(heap, last, 0);
}
return first;
}
任务调度入口:unstable_scheduleCallback
function unstable_scheduleCallback(
priorityLevel: PriorityLevel,
callback: Callback,
options?: {delay: number},
): Task {
var currentTime = getCurrentTime();
var startTime;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
// Times out immediately
timeout = -1;
break;
case UserBlockingPriority:
// Eventually times out
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
// Never times out
timeout = maxSigned31BitInt;
break;
case LowPriority:
// Eventually times out
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
// Eventually times out
timeout = normalPriorityTimeout;
break;
}
// 计算任务过期时间
var expirationTime = startTime + timeout;
// 创建新任务对象
var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
// 将任务插入最小堆(按expirationTime排序)
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
}
return newTask;
}
unstable_scheduleCallback被调用路径
unstable_scheduleCallback 主要通过以下路径被调用:
- 状态更新:setState、useState、useReducer。
- 副作用调度:useEffect、useLayoutEffect。
- 并发模式 API:startTransition、useDeferredValue。
- 根节点渲染:ReactDOM.createRoot().render()。
核心调用链路
以 setState 为例,调用链如下:
setState -> enqueueSetState -> scheduleUpdateOnFiber -> ensureRootIsScheduled -> ensureScheduleIsScheduled -> scheduleImmediateRootScheduleTask -> unstable_scheduleCallback -> requestHostCallback -> schedulePerformWorkUntilDeadline -> port2.postMessage -> port1.onmessage -> performWorkUntilDeadline -> flushWork
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
class 组件在继承 React.Component 时通过 constructor 指定 instance.updater = classComponentUpdater,enqueueSetState 就是 classComponentUpdater 对象的属性方法
任务调度(unstable_scheduleCallback)到任务执行(performUnitOfWork)的关键在于 flushWork 中 currentTask.callback 的执行。currentTask.callback 是调用 unstable_scheduleCallback 时传入的第二个参数(unstable_scheduleCallback(ImmediateSchedulerPriority, processRootScheduleInImmediateTask)):
processRootScheduleInImmediateTask -> performWorkOnRoot -> renderRootConcurrent -> workLoopConcurrentByScheduler -> performUnitOfWork
此外,performWorkOnRoot 函数内在执行完 renderRootConcurrent 后会返回 exitStatus 值,用于判断完成状态,调用 finishConcurrentRender 后提交 commitRoot
4.2 并发模式(Concurrent Mode)的底层支持
高优先级更新的插队机制(如用户输入)
Fiber 高优先级任务的插队机制 是并发模式(Concurrent Mode)的核心特性之一,它允许高优先级任务(如用户交互)中断正在执行的低优先级任务(如数据加载)
以 setState 举例说明高优先级更新的插队机制
任务调度
当触发 setState 更新时:
- 创建 update 对象并标记 lane
- 调用 scheduleUpdateOnFiber 向上收集优先级到 root.pendingLanes
enqueueSetState(inst: any, payload: any, callback) { const fiber = getInstance(inst); const lane = requestUpdateLane(fiber); const update = createUpdate(lane); update.payload = payload; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback); } update.callback = callback; } const root = enqueueUpdate(fiber, update, lane); if (root !== null) { startUpdateTimerByLane(lane, 'this.setState()'); scheduleUpdateOnFiber(root, fiber, lane); entangleTransitions(root, fiber, lane); } if (enableSchedulingProfiler) { markStateUpdateScheduled(fiber, lane); } } // react-reconciler/src/ReactFiberWorkLoop.js function scheduleUpdateOnFiber(root, fiber, lane) { // Mark that the root has a pending update. markRootUpdated(root, lane); // 更新 root.pendingLanes ensureRootIsScheduled(root); // 触发调度 } function ensureRootIsScheduled(root: FiberRoot): void { mightHavePendingSyncWork = true; ensureScheduleIsScheduled(); } function ensureScheduleIsScheduled(): void { // 在当前事件结束时,遍历每个根,并确保以正确的优先级为每个根安排了一个任务 // didScheduleMicrotask 用于防止调度冗余的 mircotask if (__DEV__ && ReactSharedInternals.actQueue !== null) { // We're inside an `act` scope. if (!didScheduleMicrotask_act) { didScheduleMicrotask_act = true; scheduleImmediateRootScheduleTask(); } } else { if (!didScheduleMicrotask) { didScheduleMicrotask = true; scheduleImmediateRootScheduleTask(); } } } function scheduleImmediateRootScheduleTask() { if (supportsMicrotasks) { scheduleMicrotask(() => { processRootScheduleInMicrotask(); }); } else { // If microtasks are not supported, use Scheduler. Scheduler_scheduleCallback( ImmediateSchedulerPriority, processRootScheduleInImmediateTask, ); } }
中断低优先级任务(scheduleTaskForRootDuringMicrotask)
function processRootScheduleInMicrotask() {
let prev = null;
// 1. 遍历全局调度链表(firstScheduledRoot → lastScheduledRoot)
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
root.next = null;
if (prev === null) {
// This is the new head of the list
firstScheduledRoot = next;
} else {
prev.next = next;
}
if (next === null) {
// This is the new tail of the list
lastScheduledRoot = prev;
}
} else {
// This root still has work. Keep it in the list.
prev = root;
if (
syncTransitionLanes !== NoLanes ||
includesSyncLane(nextLanes) ||
(enableGestureTransition && isGestureRender(nextLanes))
) {
mightHavePendingSyncWork = true;
}
}
root = next;
}
}
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
markStarvedLanesAsExpired(root, currentTime);
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
rootHasPendingCommit);
const existingCallbackNode = root.callbackNode;
if (
nextLanes === NoLanes ||
(root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
root.cancelPendingCommit !== null
) {
// Fast path: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}
if (
includesSyncLane(nextLanes) &&
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
!checkIfRootIsPrerendering(root, nextLanes)
) {
// Synchronous work is always flushed at the end of the microtask, so we
// don't need to schedule an additional task.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
} else {
// We use the highest priority lane to represent the priority of the callback.
const existingCallbackPriority = root.callbackPriority;
const newCallbackPriority = getHighestPriorityLane(nextLanes);
if (
newCallbackPriority === existingCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
// Scheduler task, rather than an `act` task, cancel it and re-schedule
// on the `act` queue.
!(
__DEV__ &&
ReactSharedInternals.actQueue !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
// The priority hasn't changed. We can reuse the existing task.
return newCallbackPriority;
} else {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performWorkOnRootViaSchedulerTask.bind(null, root),
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;
}
}
插队机制关键解析
(1)标记过期任务
markStarvedLanesAsExpired(root, currentTime);
- 作用:检查 root.pendingLanes 中是否有低优先级任务因长期未执行而“饿死”(超过 timeout 未处理)
- 插队机制:将过期的低优先级任务标记为 expiredLanes 使其升级为同步优先级(SyncLane)从而获得立即执行的机会
(2)计算下一个要处理的 lanes(getNextLanes)
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
rootHasPendingCommit
);
- 优先级比较:从 pendingLanes 中选择最高优先级的 lanes,规则包括:
- 优先选择已过期的 expiredLanes(相当于强制插队)
- 然后选择用户交互相关的 InputContinuousLane 或 DefaultLane
- 避免与正在进行的渲染任务冲突
(3)中断低优先级
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode); // 取消当前正在执行的低优先级任务
}
- 当检测到更高优先级的 newCallbackPriority 时,立即取消当前任务的回调执行。
(4)调度新任务
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performWorkOnRootViaSchedulerTask.bind(null, root)
);
Suspense的异步渲染流程
Suspense 的异步渲染流程主要围绕 异步数据加载 和 懒加载组件(Code Splitting) 两大场景展开。Suspense 的工作原理是捕获子组件抛出的 Promise,并在 Promise 未完成时显示 fallback UI。
其异步状态管理的逻辑如下:
- 当子组件(如 lazy 组件或使用 use Hook 的组件)需要等待异步操作(如数据加载或代码加载)时,它会 抛出一个 Promise。
- Suspense 会捕获这个 Promise,并进入 挂起(Suspended)状态,显示 fallback UI。
- 当 Promise 完成(resolve 或 reject),React 会重新尝试渲染子组件。
渲染流程详解
(1)Code Splitting(懒加载组件)
const LazyComponent = lazy(() => import("./Component"));
- lazy 返回一个特殊对象(LazyComponent),包含
_status(Pending/Resolved/Rejected)和_result(加载的模块)。 - 首次渲染时,readLazyComponentType 检查状态:
- 若 Pending,抛出 thenable(Promise),触发 Suspense 显示 fallback。
- 若 Resolved,返回模块并渲染
(2)数据获取(Async Data Fetching)
实验性 API unstable_createResource 允许在组件内同步读取异步数据:
const resource = unstable_createResource(fetchData);
function MyComponent() {
const data = resource.read(id); // 可能抛出 Promise
return <div>{data}</div>;
}
- resource.read 检查缓存:
- 若数据未加载,抛出 Promise,Suspense 捕获并显示 fallback。
- 若数据已加载,直接返回6。
- React 在微任务阶段重新尝试渲染完成加载的组件
4.3 实际案例:startTransition的工作原理
4.4 实际案例:setState 的完整更新流程
React 控制的实例化
组件定义阶段
// 用户编写的组件
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return <div>{this.state.count}</div>;
}
}
React 内部实例化过程
当 React 需要创建组件实例时,不会直接调用 new Demo(),而是通过以下路径:
graph TD
A[ReactDOM.render] --> B[创建根Fiber]
B --> C[开始渲染]
C --> D[beginWork阶段]
D --> E[处理类组件]
E --> F[constructClassInstance]
F --> G[注入updater]
ReactDOM.render 是绑定在 ReactDOMRoot.prototype 上的原型方法,内部调用 react-reconciler/src/ReactFiberReconciler 中的 updateContainer 方法
import {updateContainer} from 'react-reconciler/src/ReactFiberReconciler';
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMRoot.prototype.render =
// $FlowFixMe[missing-this-annot]
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
// ……
updateContainer(children, root, null, null);
};
updateContainer 会调用 scheduleUpdateOnFiber 方法,从而一步一步按之前讲过的调用链路触发 performUnitOfWork 函数执行,并在 beginWork 中通过 updateClassComponent 调用 constructClassInstance、mountClassInstance等方法注入 updater
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}
function updateContainerImpl(
rootFiber: Fiber,
lane: Lane,
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): void {
const update = createUpdate(lane);
const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
startUpdateTimerByLane(lane, 'root.render()');
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
workInProgress.elementType === Component,
);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
}
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
}
}
setState 的执行逻辑
setState 是绑定在 React.Component.prototype 上的原型方法:
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
}
在 ReactDOM.render 初始化组件时,已经通过 constructClassInstance 实例化了 Component,并注入了 updater:
function constructClassInstance(
workInProgress: Fiber,
ctor: any,
props: any,
): any {
let instance = new ctor(props, context);
instance.updater = classComponentUpdater;
}
const classComponentUpdater = {
// $FlowFixMe[missing-local-annot]
enqueueSetState(inst: any, payload: any, callback) {
const fiber = getInstance(inst);
const lane = requestUpdateLane(fiber);
const update = createUpdate(lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback);
}
update.callback = callback;
}
const root = enqueueUpdate(fiber, update, lane);
if (root !== null) {
startUpdateTimerByLane(lane, 'this.setState()');
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitions(root, fiber, lane);
}
if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
},
// ……
};
这里的 let instance = new ctor(props, context); 即是 new Component(props, context)