深入理解 React Fiber:工作单元、渲染与提交以及优先级通道
了解 React Fiber 的真正含义,为何旧的堆栈协调器会阻塞主线程,以及工作单元、两个阶段和通道是如何实现并行渲染的。
“Fiber”是React中那些被频繁提及却很少得到真正解释的术语之一。开发者们听到它被描述为一种新的引擎、Virtual DOM的替代品,或是与Hooks相关的概念,但这些描述其实都不完全准确。本指南从基础开始讲解这一概念:首先介绍React在16版本之前的问题,接着说明Fiber是什么,渲染过程如何分为两个阶段,以及调度和优先级机制是如何运作的。读完之后,你应该能够准确解释Fiber,识别常见的误解,并理解为何像startTransition这样的API依赖于它。
一句话概括Fiber
React Fiber是随React 16一同推出的内部同步架构。它的作用在于让渲染过程变得可控制——React不会将更新视为一个不可分割的任务来处理,而是将其拆分成多个小单元,逐一处理并按重要性排序。
通过对比最能看出这种变化。在Fiber出现之前,更新是从开始到结束的线性过程:
Before Fiber:
Update
↓
Render entire tree
↓
Commit changes
↓
Done
而在Fiber中,中间会出现一个阶段,在此阶段中任务会被拆分,这些拆分后的部分可以在内容显示到屏幕之前进行排序、暂停或重新继续处理:
After Fiber:
Update
↓
Break work into units
↓
Process units
↓
Prioritize / pause / resume when appropriate
↓
Commit changes
这个额外的阶段为React中几乎所有的现代渲染功能提供了基础。要了解为何需要重写代码,就需要看看之前的实现方式。
堆栈同步器及其缺陷
16版本之前的React使用的是通常被称为堆栈协调器的机制。其整体流程早已为人所熟知:状态变化会触发渲染,新渲染结果会与之前的结果进行对比协调,最终更新DOM。
State Change
↓
Render
↓
Reconciliation
↓
DOM Updates
问题在于这种协调操作是同步进行的。之所以叫“堆栈协调器”,是因为协调器通过普通的递归函数调用来遍历节点结构,因此工作的进展就体现在JavaScript调用堆栈上。一旦React开始处理更新操作,就不存在中途停止的合理方式,因为停止意味着要回溯该调用堆栈并丢失当前的执行位置。试想一个规模较大的应用程序:
App
│
├── Header
├── Sidebar
├── Dashboard
│ ├── Chart
│ ├── Table
│ └── Statistics
├── Notifications
└── Footer
如果某次更新影响了这棵组件树中的大部分节点,React会一次性遍历所有受影响的组件,从第一个到最后一个,且不会将控制权交还:
Start rendering
↓
Component A
↓
Component B
↓
Component C
↓
Component D
↓
Component E
↓
...
↓
Finish
输出结果本身没有问题,问题出在时间上。React无法中断当前工作并在之后再继续,因此其他所有操作都不得不等待。
为何漫长的渲染会损害用户体验
JavaScript并没有专用的主线程。浏览器使用同一条线程来执行事件处理、计算布局、绘制像素以及生成帧:
Browser Main Thread
│
├── JavaScript
├── Event Handling
├── Layout
├── Paint
└── Rendering
当某个脚本长时间占用线程时,所有其他任务都会在其后面排队等待。从用户的角度来看,事件的处理流程如下:
Click
↓
React starts large update
↓
Main thread remains busy
↓
Browser can't respond quickly
↓
UI feels slow
点击操作响应延迟,按键输入也有明显滞后,动画播放也不流畅。在小型应用中,渲染过程时间很短,因此没人会注意到这些问题。但随着应用规模扩大、交互增多,框架就需要能够控制渲染工作在何时执行以及一次执行多少内容。Fiber正是为填补这一空白而设计的。
核心理念:将工作视为可调度单元
Fiber背后的思维模型可以用一句话概括:渲染被拆分成React可以调度和排序的可行工作单元。
在旧模型下,发送给协调器的指令实际上只是一个单一命令:
"Render this entire tree."
而在Fiber模式下,React可以分别处理各个部分,并决定哪些内容应优先处理:
"Here is one piece of work."
"Here is another piece."
"Which work should I process first?"
通过使用独立的代码块而非深度递归调用,React可以在任意一个代码块执行完毕后暂停,检查是否有更重要的任务需要处理,之后再继续执行。
纤维到底是什么
纤维是React内部任务结构中的一个基本单元,实际上就是一个普通的JavaScript对象。在实践中,参与状态同步的每个组件或元素大致对应一个纤维。以这棵简单的组件树为例:
App
│
├── Header
├── Sidebar
└── Content
从概念上讲,React维护着一个由纤维构成的并行结构:
Fiber Tree
App Fiber
│
├── Header Fiber
├── Sidebar Fiber
└── Content Fiber
真正的 Fiber 对象所包含的信息远不止一个名称。它们记录了与父节点、子节点及兄弟节点的关联关系,组件的类型与标识,待处理的属性和状态,以及任务完成时需要执行的操作。正是这些明确的关联关系使得 React 能够通过循环而非递归的方式来遍历节点树,这也为实现任务的暂停与继续提供了可能。不过从整体架构来看,只需记住一个要点:Fiber 是 React 渲染与状态同步工作的基本单元。
Fiber 并非 Virtual DOM 的替代品
有一种常见的说法认为 Fiber “取代了 Virtual DOM”,但事实并非如此。二者解决的是不同的问题。
Virtual DOM 是对界面应呈现状态的描述,React 会将其与之前的描述进行对比,从而确定哪些部分需要更改:
Virtual DOM
↓
"What should the UI look like?"
Fiber是React用来组织并执行实现目标所需工作的机制:
Fiber
↓
"How should React organize and process the work required to get there?"
它们共同发挥作用,但一个代表UI,另一个则是处理任务的方式。若想更深入地了解两者的对比,请参阅React的虚拟DOM差异比较机制如何决定更新内容。
更新过程中的Fiber树结构
React会在整个应用生命周期内维护其Fiber树。一个更复杂的示例展示了子树如何嵌套在其中:
App
│
┌─────────┼─────────┐
↓ ↓ ↓
Header Sidebar Content
│
┌──────┴──────┐
↓ ↓
Chart Table
当props或状态发生变化时,React会利用这一结构找出需要处理的分支,而跳过其余部分。
实现渲染的可中断性
这种设计最重要的好处在于可以暂停渲染过程。试想一下那些需要分多个步骤完成的更新:
Large Update
↓
Work 1
↓
Work 2
↓
Work 3
↓
Work 4
在使用同步协调器时,这四个步骤必须依次执行。而借助 Fiber,React 可以先处理其中几步,然后让出控制权以便浏览器响应用户输入或绘制画面,之后再继续执行剩余步骤:
Work 1
↓
Work 2
↓
Pause
↓
Browser gets an opportunity to handle other work
↓
Resume
↓
Work 3
↓
Work 4
这本身并非速度优化措施,因为总工作量并未改变。只是这些任务不再独占线程,从而让 React 有空间安排其他任务的执行。
两个阶段:确定变更,然后应用变更
现代 React 并不会将更新视为从渲染到 DOM 的单一过程:
Render → DOM
相反,每次更新都要经历两个不同的阶段:
React Update
│
↓
Render Phase
│
↓
Commit Phase
│
↓
DOM
渲染阶段用于计算下一个用户界面
在渲染阶段,React 会确定界面当前应有的样子。具体来说,它会:
- 执行组件函数并处理待处理的更新
- 构建或同步纤维树结构
- 找出与当前树结构不同的部分
- 整理出需要应用的所有变更列表
用图表表示的话,就是将之前的树结构与新的更新内容输入,最终输出所需处理的操作描述:
Previous Tree
+
New Updates
↓
Reconciliation
↓
New Work
由于此阶段不会触及 DOM,因此在现代 React 中可以中断该流程。React 可以在后台计算下一棵渲染树的大部分内容,而没有人会看到任何中间状态。这也解释了为何 React 要求渲染过程不能存在副作用:在并发渲染模式下,一个组件可能在结果被正式应用之前被多次渲染;而在开发模式下,严格模式会故意多次调用渲染逻辑,以便找出那些违反此假设的代码。
应用阶段负责正式应用结果
一旦变更确定,React 便会将其正式应用:
Render Phase
↓
Changes determined
↓
Commit Phase
↓
DOM updated
真正的数据修改发生在应用阶段:此时会插入、更新或删除 DOM 节点,附加引用,并执行布局相关的操作。区分这两个阶段的一个实用方法是:
Render Phase
"Let's figure out what needs to change."
Commit Phase
"Now apply those changes."
为何无法暂停应用阶段
如果暂停如此有用,为何不在所有地方都暂停呢?因为屏幕内容必须保持一致。试想一下,如果 React 在向 DOM 写入数据时中途停止:
Update A
↓
DOM partially changed
↓
Pause
↓
Update B
用户将会看到本不应同时存在的旧界面与新界面的混杂景象。因此 React 将不同任务分开处理:变更的生成过程可能会被中断、重新开始或丢弃,但提交操作会一次性应用最终结果。这一划分是理解现代 React 的关键。
调度:并非所有更新都同等重要
由于任务被分解为多个单元,React 可以开始判断哪些任务最为重要。有些更新显然比其他更新更为紧急。针对这一点:
User clicks a button
在当前时刻对用户来说比这个更重要:
Rendering a large list somewhere else
同样地,这个也:
Typing in an input
操作响应必须即时。页面其他地方进行的复杂更新不应导致字符输入滞后于键盘输入。Fiber提供了相应的结构,使React能够识别这类差异并有序地处理任务。
通道:React如何标记优先级
在内部,现代React通过通道来管理更新,这些通道用于编码更新的优先级。应用程序代码几乎从不直接操作通道;React利用它们来决定在每次渲染时先处理哪些待处理的更新,哪些可以延迟。简化后的示意图如下:
Updates
│
┌───────────┼───────────┐
↓ ↓ ↓
Urgent Normal Deferred
│ │ │
↓ ↓ ↓
Process Process Process
sooner normally later
当三个相关概念同时出现时,将它们区分开来会更有帮助:
Fiber
↓
Represents work
Scheduler
↓
Helps coordinate when work should happen
Lanes
↓
Represent priority of updates
Fiber用于描述任务内容,调度器决定其执行时间,而通道则体现每次更新的紧急程度。这些组件的具体实现会在React的不同版本中发生变化,因此应将其视为概念模型,而非当前源代码的描述。
旧版与新版,逐步对比
在Fiber出现之前,同步化且基于堆栈的机制负责任务处理,一旦开始就会持续执行:
Update
↓
Reconcile
↓
Continue
↓
Continue
↓
Continue
↓
Finish
几乎无法中断任务处理,也无法优先处理某次更新。
Fiber架构将调度决策融入到处理流程中:
Update
↓
Create / schedule work
↓
Process Fiber units
↓
Prioritize
↓
Pause / resume / restart when appropriate
↓
Complete render
↓
Commit
将两种处理方式放在一起对比,差异便一目了然:
BEFORE FIBER:
Component Tree
↓
Synchronous Reconciliation
↓
Finish Everything
↓
Commit
AFTER FIBER:
Component Tree
↓
Fiber Tree
↓
Units of Work
↓
Prioritize / Schedule
↓
Render
↓
Commit
重点并非React的速度变快,而是它能够掌控渲染任务的执行方式和时机。
常见误解
Fiber不会增加线程
Fiber不会将React分配到多个线程中执行:
React
├── Thread 1
├── Thread 2
└── Thread 3
React的JavaScript仍然在浏览器的主线程上运行。Fiber是一种协作式调度方式:React会在不同的工作单元之间主动让出执行权,以便其他任务有机会运行。这与Web Workers有本质区别,后者确实是在独立的线程上执行代码的。
并发不等于同时
Fiber使得并发渲染成为可能,但“并发”这个词很容易被误解。它并不意味着React会在同一时刻渲染所有内容,而是指React可以在不阻塞整个应用进程的情况下准备更新。典型的执行顺序为:
Low-priority update
↓
React starts rendering
↓
Higher-priority update arrives
↓
React can prioritize the important work
↓
Continue / restart lower-priority work
当有更紧急的任务出现时,低优先级的渲染可以暂时搁置,之后再继续执行或从头开始。这正是现代 React 中许多流畅交互背后的机制。关于在框架环境中的实现方式,文章部分预渲染与并发渲染详解从 Next.js 的角度进行了阐述。
Fiber 并非“一次只渲染一个组件”
人们也容易将 Fiber 想象成 React 先渲染一个组件,然后再渲染下一个,依此类推:
Component 1
Component 2
Component 3
这种理解太过简单。React 的遍历、批量处理和调度机制比简单的列表结构要复杂得多;Fiber 提供的作业模型比旧的递归方式细致得多。
实际应用中的过渡效果
在应用代码中,过渡操作正是 Fiber 调度机制的体现。以一个用户刚刚输入内容的搜索框为例:
User types: "rea"
该按键操作会触发两种不同的处理流程:
1. Update the input immediately
2. Update a huge search result list
用户能看到的是输入内容的实时更新;而重新生成冗长的结果列表可能会耗费较多资源。React 允许通过将状态更新操作包裹在 startTransition 中,将第二种处理标记为非紧急任务:
startTransition(() => {
setSearchResults(results);
});
用户输入的字符会被视为紧急更新,从而确保输入框始终能快速响应:
User Input
↓
Urgent Update
↓
Keep UI responsive
结果列表则被视作过渡操作,React 可能会以较低的优先级来渲染它,而且如果用户持续输入,还会中断其渲染过程:
Search Results
↓
Transition
↓
Can be handled with lower priority
有两点实用提示。首先,过渡机制并不会降低昂贵操作的成本;它只是防止这些操作阻塞紧急更新,因此速度较慢的列表仍可能从记忆化或虚拟化中受益。其次,输入内容的状态应保持在过渡机制之外,否则输入操作本身也会被延迟处理,导致字段响应迟缓。如果没有 Fiber 提供的可中断渲染阶段,所有这些调度机制都无法实现。
为何 React 需要新的基础
在 Fiber 出现之前,React 已经具备了人们通常认为它应具备的大部分功能:
- 虚拟 DOM
- 状态同步机制
- 组件模型
- 高效的 DOM 更新
这次重新设计是为了面向未来,而非修复现有缺陷。应用程序正在多个方面同时发展:
Larger
+
More interactive
+
More data-driven
+
More complex
这种增长意味着 React 不仅需要关注发生了什么变化,还需要回答诸如某项操作应在何时执行、某个更新的重要性如何、是否可以暂停操作、是否有其他更新应优先处理,以及如何避免阻碍用户最在意的交互等问题。Fiber 正是能够解答这些问题的架构。
仪表板场景
想象一个包含多个复杂区域的分析仪表板:
Dashboard
│
├── Navigation
├── Filters
├── Revenue Chart
├── User Chart
├── Large Data Table
└── Notifications
当用户更改筛选条件时,简单的实现方式可能会一次性重新渲染图表和大型表格,导致筛选控件的响应变得迟缓。而在 Fiber 架构下,同样的交互会通过分层次处理的任务流来执行:
User changes filter
↓
Update begins
↓
React performs reconciliation
↓
Work is represented through Fiber
↓
Updates can be prioritized
↓
Rendering completes
↓
Commit changes
图表和表格仍需重新计算。不过在 React 处理这些工作时,响应速度得到了提升。
Fiber 与相关概念的对比
Fiber 与虚拟 DOM
虚拟 DOM 是 React 用来判断哪些部分需要更改的 UI 表示形式:
UI State
↓
Virtual Representation
Fiber 是 React 用于表示和处理渲染工作的内部架构与数据结构:
Component / Element
↓
Fiber
↓
Reconciliation + Scheduling
二者共同构成了 React 的渲染系统:
Virtual DOM
+
Fiber Architecture
↓
React Rendering System
相关但不可互换。
Fiber 与 Web Workers
它们解决的是完全不同的问题。Fiber 关注的是:
Rendering
Reconciliation
Scheduling
Prioritization
相比之下,Web Worker 则负责:
Running JavaScript
outside the main UI execution context
其结构如下,将繁重的计算任务从负责 UI 的线程中分离出来:
Main Thread
│
├── UI
├── React
│
↓
Web Worker
│
↓
Heavy computation
Fiber从不会将React的渲染任务转移到工作线程中。如果你有那些不涉及渲染但需要大量CPU算力的任务,比如数据解析或数值计算,工作线程依然是合适的工具;专用工作线程、共享工作线程和服务工作线程的相关文章详细介绍了这些选项。
这对你的代码意味着什么
在日常开发中,你永远不会直接操作Fiber。你需要做的就是编写组件:
function App() {
return <Dashboard />;
}
然后触发更新操作:
setState(newValue);
React会在后台将这些操作转换为Fiber任务。你的代码位于整个处理流程的顶层,而底层的具体实现则无需过多关注:
Your React Code
↓
React APIs
↓
Fiber Architecture
↓
Reconciliation
↓
Scheduling / Prioritization
↓
Commit
↓
DOM
你永远不应手动创建纤维节点。你需要遵循这样的思维模式:保持渲染逻辑的纯粹性,将副作用处理到效果函数或处理器中,而对于成本高昂且非紧急的更新,则使用过渡效果来处理。
最简总结
旧版的协调器遵循一条简单的规则:
"Start rendering → keep going → finish."
Fiber则采用不同的规则:
"Break rendering into work → decide how to schedule it
→ complete the render → commit the result."
这种差异正是整个架构转变的精髓所在。
总结
堆栈协调器多年来为React提供了良好的支持。而Fiber则用于处理那些已超出其处理能力的应用场景。这一演变过程是从控制力有限的模式:
Old React
↓
Synchronous Stack Reconciler
↓
Limited control over rendering work
发展为工作任务能够被明确定义、调度并设置优先级的模式:
React Fiber
↓
Fiber Tree + Units of Work
↓
More flexible reconciliation
↓
Scheduling + Prioritization
↓
Concurrent Rendering Capabilities
当有人询问什么是 React Fiber 时,称其为“新的渲染引擎”其实低估了它的作用。更准确的答案是,Fiber 是 React 的内部协调架构,它将渲染过程视为一系列任务单元,从而使 React 能够在适当的时候安排、优先处理、中断或恢复这些任务。
关键要点
- Fiber 于 React 16 中出现,取代了递归式的同步栈协调器。
- 一个 Fiber 对象代表一个渲染任务单元,这些对象被连接成一棵树,React 可以遍历并暂停这棵树中的处理流程。
- 渲染阶段负责计算变化,且可以被中断;而提交阶段则会在一个连续不断的步骤中应用这些变化。
- Lanes 与调度器利用 Fiber 的结构,确保紧急更新比延迟更新具有更高的优先级。
相关阅读
- React的调度器与事件循环:究竟谁来决定操作的执行时机 — 了解React的协作式调度器如何在JavaScript事件循环中运行,为何过渡效果需要让出控制权,以及为何没有任何调度器能够拯救被阻塞的线程。
- 了解 React Lanes:位掩码如何编码更新优先级 — 了解 React 如何利用位掩码将多种更新优先级编码为单个整数,以及为何在调度时选择位运算而非简单的布尔标志。