首页 / 文章 / 深入理解 React Fiber:工作单元、渲染与提交以及优先级通道

深入理解 React Fiber:工作单元、渲染与提交以及优先级通道

了解 React Fiber 的真正含义,为何旧的堆栈协调器会阻塞主线程,以及工作单元、两个阶段和通道是如何实现并行渲染的。

3495 词

“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 的结构,确保紧急更新比延迟更新具有更高的优先级。
  • Fiber并非虚拟DOM也不是多线程技术,而是基于主线程的协作式调度机制。
  • 并发渲染与过渡效果正是建立在这一基础之上,但它们只是管理那些耗时的操作,而非彻底消除这些操作。
  • 相关阅读