Published on

React Fiber

Authors
  • avatar
    Name
    Deng Hua
    Twitter

翻译至: An Introduction to React Fiber - The Algorithm Behind React

在本文中,我们将了解 React Fiber——React 背后的核心算法。 React Fiber 是 React 16 中的新协调算法。您很可能听说过 React 15 中的 virtualDOM。它是旧的协调器算法(也称为 Stack Reconciler),因为它内部使用堆栈。相同的协调器与不同的渲染器(如 DOM、Native 和 Android 视图)共享。因此,将其 virtualDOM 调用可能会导致混乱。

目录

介绍

React Fiber 是对旧协调器的完全向后兼容的重写。 React 的这种新协调算法称为 Fiber Reconciler。这个名字来自 Fiber,它用它来表示 DOM 树的节点。我们将在后面的部分中详细介绍 Fiber

Fiber 协调器的主要目标是增量渲染、UI 动画和手势的更好或更平滑的渲染以及用户交互的响应能力。协调器还允许您将工作划分为多个块并将渲染工作划分为多个帧。它还增加了定义每个工作单元的优先级以及暂停、重用和中止工作的能力。

在计算新的渲染更新时,React 会多次引用主线程。因此,高优先级工作可以跳过低优先级工作。 React 为每个更新内部定义了优先级。

在讨论技术细节之前,我建议您学习以下术语,这将有助于理解 React Fiber。

前置概念

Reconciliation(协调算法)

正如 React 官方文档中所解释的,协调算法是比较两个 DOM 树的算法。当 UI 第一次渲染时,React 会创建一棵节点树。每个单独的节点代表 React 元素。它创建一个虚拟树(称为 virtualDOM),它是渲染 DOM 树的副本。在 UI 进行任何更新后,它会递归地比较两棵树中的每个树节点。然后传递累积的变化到渲染器。

Scheduling(调度)

正如 React 文档中所解释的,假设我们有一些低优先级的工作(例如大型的计算函数或最近获取的元素的渲染)和一些高优先级的工作(例如动画)。应该有一个选项来优先处理高优先级工作而不是低优先级工作。在旧的堆栈协调器实现中,递归遍历和调用整个更新树的渲染方法发生在单个流程中。这可能会导致丢帧。

调度安排可以是基于时间的,也可以是基于优先级的。应根据最后时间安排更新。优先级高的工作应优先于优先级低的工作。

requestIdleCallback

requestAnimationFrame下一个动画帧之前调用高优先级函数。同样,requestIdleCallback 会在帧末尾的空闲时间调用低优先级或非必要函数。

requestIdleCallback(lowPriorityWork);

这里演示了requestIdleCallback的用法。 lowPriorityWork 是一个回调函数,将在帧结束时的空闲时间内调用。

function lowPriorityWork(deadline) {
  // 当前前帧剩余的时间 > 0 且 任务列表不为空,则可以执行相关任务
  while (deadline.timeRemaining() > 0 && workList.length > 0) {
    performUnitOfWork();
  }

  // 当前前帧剩余的时间 < 0,还有尚未执行的任务,将放到下一次帧空闲时间执行
  if (workList.length > 0) {
    requestIdleCallback(lowPriorityWork);
  }
}

当调用此回调函数时,它会获取deadline参数对象。正如您在上面的代码片段中看到的,timeRemaining 函数会返回最新的剩余空闲时间。如果这个时间大于零,我们可以完成所需的工作。如果工作没有完成,我们可以在下一帧的最后一行再次安排它。

所以,现在我们可以继续了解 Fiber 对象,并了解 React Fiber 的工作原理。

fiber 结构

fiber(小写“f”)是一个简单的 JavaScript 对象。它代表了 React 元素或 DOM 树的一个节点。这是一个工作单元。相比之下,Fiber 是 React Fiber reconciler(调节器)。

此示例显示了一个在根 div 中渲染的简单 React 组件。

function App() {
  return (
    <div className="wrapper">
      <div className="list">
        <div className="list_item">List item A</div>
        <div className="list_item">List item B</div>
      </div>
      <div className="section">
        <button>Add</button>
        <span>No. of items: 2</span>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

这是一个简单的组件,它显示了从组件状态获取的数据项列表。(用两个列表项替换了.mapiteration类函数 ,只是为了让这个例子看起来更简单)还有一个<button><span>,展示了列表项的数量。

正如前面提到的, Fiber 代表 React 元素。第一次渲染时,React 会遍历每个 React 元素并创建一棵 fiber 树(我们将在后面的部分中看到它如何创建这棵树)。

它为每个单独的 React 元素创建一个 Fiber,如上面的代码示例所示。它将为 div 创建一个fiber,比如叫W,它具有值为wrapper的class。然后,fiber L 为 div,它有一个list的class,接着往下。我们将两个class为list_item的fiber命名为 LALB

在后面的章节中,我们将看到它如何迭代以及树的最终结构。虽然我们称之为树,但 React Fiber 创建的是一个节点链表,其中每个节点都是一个 fiber。父节点、子节点和同级节点之间也存在关系。

React 通过返回一个key 指针来指向父节点,任何子fiber 在调用结束后都会返回父节点。因此,在上面的示例中,LA 的返回是 L,它的同级节点是 LB

那么,这个fiber object到底是什么样子的呢?

以下是 React 代码库中定义的类型定义。我删除了一些额外的props,并保留了一些注释来理解属性的含义。您可以在 React 代码库中找到详细的结构。

export type Fiber = {
  // Tag identifying the type of fiber.
  tag: TypeOfWork,

  // Unique identifier of this child.
  key: null | string,

  // The value of element.type which is used to preserve the identity during
  // reconciliation of this child.
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  type: any,

  // The local state associated with this fiber.
  stateNode: any,

  // Remaining fields belong to Fiber

  // The Fiber to return to after finishing processing this one.
  // This is effectively the parent.
  // It is conceptually the same as the return address of a stack frame.
  return: Fiber | null,

  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  // The ref last used to attach this node.
  ref: null | (((handle: mixed) => void) & { _stringRef: ?string, ... }) | RefObject,

  // Input is the data coming into process this fiber. Arguments. Props.
  pendingProps: any, // This type will be more specific once we overload the tag.
  memoizedProps: any, // The props used to create the output.

  // A queue of state updates and callbacks.
  updateQueue: mixed,

  // The state used to create the output
  memoizedState: any,

  mode: TypeOfMode,

  // Effect
  effectTag: SideEffectTag,
  subtreeTag: SubtreeTag,
  deletions: Array<Fiber> | null,

  // Singly linked list fast path to the next fiber with side-effects.
  nextEffect: Fiber | null,

  // The first and last fiber with side-effect within this subtree. This allows
  // us to reuse a slice of the linked list when we reuse the work done within
  // this fiber.
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,
};

React Fiber 在首次创建和更新时会做什么?

接下来,我们将看到 React Fiber 如何创建链表树,以及它在有更新时会做什么。

在此之前,让我们解释一下什么是current tree(当前树)workInProgress树,以及树遍历是如何发生的。

当前刷新以渲染UI 的树称为 current tree。用于渲染当前 UI。每当有更新时,Fiber会构建一个 workInProgress 树,该树根据更新后的 React 元素数据来创建。React 在此 workInProgress 树上执行工作,并将此树用于下一次渲染。在 UI 上渲染此 workInProgress 树后,它将成为当前树。

Current 和 workInProgress 树

Fiber树遍历的方式如下:

  • Start: Fiber 从最顶层的 React 元素开始遍历,并为其创建一个 fiber 节点。

  • Child: 然后,它会转到子元素,并为该元素创建一个 fiber 节点。这个过程一直持续到leaf element(叶子元素)。

  • Sibling: 现在,它会检查是否有同级元素。如果有同级元素,它会遍历同级子树,直到同级元素的叶子元素。

  • Return:如果没有兄弟姐妹,则返回到父级。

每个 Fiber 都有一个子属性(如果没有子属性则为 null 值)、兄弟属性和父属性(正如您在前面部分中看到的 Fiber 的结构)。这些是 Fiber 中作为链表运作的指针。

 React Fiber 树遍历

基本是一个深度优先的前序遍历

让我们举同样的例子,但让我们给对应于特定 React 元素的fiber命名。

function App() {    // App
  return (
    <div className="wrapper">    // W
      <div className="list">    // L
        <div className="list_item">List item A</div>    // LA
        <div className="list_item">List item B</div>    // LB
      </div>
      <div className="section">   // S
        <button>Add</button>   // SB
        <span>No. of items: 2</span>   // SS
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));  // HostRoot

首先,我们将快速介绍创建树的mounting(挂载)阶段,之后,我们将看到更新以及背后的详细逻辑。

初始渲染

<App>组件在root div中渲染,其idroot

  1. 在进一步遍历之前,React Fiber 创建根fiber。每棵 Fiber 树都有一个根节点。在我们的例子中,它是 HostRoot。如果我们在 DOM 中导入多个 React 应用程序,则可以有多个根节点。

  2. 第一次渲染之前,没有任何树产生。 React Fiber 遍历每个组件的渲染函数的输出,并在树中为每个 React 元素创建一个 Fiber 节点。它使用 createFiberFromTypeAndProps 将 React 元素转换为 Fiber。

    React 元素可以是类组件或host(宿主)组件,例如 divspan。对于类组件,它创建一个实例,对于host组件,它从 React Element 获取数据/属性。

  3. 因此,如示例所示,它创建了一个 Fiber。更进一步,它又创建了一个fiber W,然后它转到子 div 并创建了一个fiber L

  4. 依此类推,它继续为其子元素创建了一个fiber LAfiber LB

  5. fiber LA 的返回到的fiber为 fiber LL也可称为父fiber)。

所以,这就是最终fiber树的样子。

React Fiber 关系

这就是使用子指针、同级指针和返回指针连接树节点的方式。也是上文所说的“每个 Fiber 都有一个子属性(如果没有子属性则为 null 值)、兄弟属性和父属性”

更新阶段

现在,让我们讨论第二种情况,即更新 —— 比如 setState

所以,此时,Fiber已经有了当前树。对于之后的每次更新,它都会构建一个 workInProgress 树。它从根fiber开始,遍历树直到叶节点。

与初始渲染阶段不同,它不会为每个 React 元素创建新的 Fiber,它只是为该React元素使用之前就存在的fiber,并在这个更新阶段中合并来自更新元素的新的 data/props

早些时候,在 React 15 中,堆栈协调器是同步的。因此,更新将递归地遍历整个树并复制该树。假设在此期间,如果出现比此优先级更高的其他更新,则无法中止或暂停第一个更新并执行第二个更新。

React Fiber 将更新划分为多个工作单元。它可以为每个工作单元分配优先级,并且能够在不需要时暂停、重用或中止工作单元。它在多个帧中调度这些工作并使用 requestIdleCallback

每个更新都能像动画一样定义其优先级,如用户输入的优先级高于从获取的数据中渲染项目列表的优先级。 Fiber 使用 requestAnimationFrame 进行较高优先级更新,使用 requestIdleCallback 进行较低优先级更新。因此,在调度工作时,Fiber 会检查当前更新的优先级和 deadline(帧结束后剩余空闲时间)。

假设当前有高优先级待处理的工作,又或者没有足够deadline(帧结束空闲时间),那么Fiber可以在单个帧之后调度多个工作单元。下一组工作单元将继续到其他帧中等待执行。这是Fiber 可以暂停、重用和中止工作单元的原因。

那么,让我们看看在这些已安排的工作中实际发生了什么。主要有两个阶段:render phases(渲染阶段)和commit phases(提交阶段)。

渲染阶段

实际的树遍历和deadline的使用发生在这个阶段。这是Fiber的内部逻辑,因此此阶段对Fiber树所做的更改对用户来说是不可见的。因此 Fiber 可以暂停、中止或划分多个帧上的工作。

我们可以将这个阶段称为 reconciliationv phase(协调阶段)。Fiber 从 fiber树的根部开始遍历,对每根fiber进行处理。每个工作单元都会调用workLoop函数来执行工作。我们可以将工作的处理分为两个步骤:

begin(开始步骤)和complete(完成步骤)。

Begin Step

如果您从React代码库中找到workLoop函数,它会调用performUnitOfWork,该函数将nextUnitOfWork作为参数。它只是要执行的工作单元。 PerformUnitOfWork函数内部调用beginWork函数。这是fiber实际上的工作地方,而performUnitOfWork 正是迭代发生的地方。

beginWork 函数内部,如果fiber没有任何待处理的工作,它只会退出(跳过)fiber,而不进入开始阶段。这就是在遍历大的fiber树时,Fiber 会跳过已经处理过的 Fiber,直有待处理工作的 Fiber。

如果您看到大的 beginWork 函数代码块,我们会找到一个 switch 块,它根据 Fiber 标签调用相应的 Fiber 更新函数。就像host组件的 updateHostComponent 一样。用这些函数更新fiber。

如果有子fiber,则 beginWork 函数返回子fiber;如果没有子fiber,则返回 null。 PerformUnitOfWork 函数不断迭代并调用子fiber,直到到达叶节点。对于叶节点,beginWork 会直接返回 null,因为没有任何子节点,并且performUnitOfWork 函数调用completeUnitOfWork 函数。现在让我们看看complete step。

Complete Step

completeUnitOfWork函数通过调用completeWork函数来完成当前的工作单元。如果有任何执行下一个工作单元的fiber,则completeUnitOfWork返回同级fiber,否则如果没有工作则完成返回(父)fiber。这一直持续到返回为 null,即直到到达根节点。

渲染阶段的结果是生成一个effect列表(副作用)。这些效果就像插入、更新或删除host组件的节点,或者调用类组件的节点的生命周期方法。fiber上标有相应的effect标签。

渲染阶段结束后,Fiber 将准备好提交更新。

提交阶段

这是将完成的工作用于在UI上渲染的阶段。由于此阶段的结果将对用户可见,因此不能分为部分渲染。这个阶段是一个同步阶段

在此阶段开始时,Fiber 已经有在 UI 上渲染的当前树finishedWork 或在渲染阶段构建的 workInProgress 树effect list

effect列表是fiber的链表,它有副作用。因此,它是渲染阶段 workInProgress 树的节点子集,具有副作用(更新)。effect列表节点使用 nextEffect 指针链接。

此阶段调用的函数是completeRoot

在这里,workInProgress 树成为当前树,因为它用于渲染 UI。实际的 DOM 更新(例如插入、更新、删除和调用生命周期方法)或与 refs 相关的更新发生在effect列表中的节点上。

这就是 Fiber reconciler(协调器)的工作原理。

总结

文章介绍了 React Fiber 协调器如何将工作划分为多个工作单元的。它设置每项工作的优先级,并使暂停、重用和中止工作单元成为可能。在 Fiber 树中,每个fiber都是链表的一个节点,它们通过子引用、兄弟引用和返回引用连接起来。

这是一份详细记录的资源列表,您可以找到它来了解有关 React Fiber 的更多信息。


参考:

React 调度原理(scheduler)

reconciler 运作流程

Hook 原理(概览)

End.