- Published on
React Fiber
- Authors
- Name
- Deng Hua
翻译至: 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'));
这是一个简单的组件,它显示了从组件状态获取的数据项列表。(用两个列表项替换了.map
和 iteration
类函数 ,只是为了让这个例子看起来更简单)还有一个<button>
和<span>
,展示了列表项的数量。
正如前面提到的, Fiber 代表 React 元素。第一次渲染时,React 会遍历每个 React 元素并创建一棵 fiber 树(我们将在后面的部分中看到它如何创建这棵树)。
它为每个单独的 React 元素创建一个 Fiber,如上面的代码示例所示。它将为 div 创建一个fiber,比如叫W
,它具有值为wrapper
的class。然后,fiber L
为 div,它有一个list
的class,接着往下。我们将两个class为list_item
的fiber命名为 LA
和 LB
。
在后面的章节中,我们将看到它如何迭代以及树的最终结构。虽然我们称之为树,但 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 树后,它将成为当前树。
Fiber树遍历的方式如下:
Start: Fiber 从最顶层的 React 元素开始遍历,并为其创建一个 fiber 节点。
Child: 然后,它会转到子元素,并为该元素创建一个 fiber 节点。这个过程一直持续到leaf element(叶子元素)。
Sibling: 现在,它会检查是否有同级元素。如果有同级元素,它会遍历同级子树,直到同级元素的叶子元素。
Return:如果没有兄弟姐妹,则返回到父级。
每个 Fiber 都有一个子属性(如果没有子属性则为 null
值)、兄弟属性和父属性(正如您在前面部分中看到的 Fiber 的结构)。这些是 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中渲染,其id
为 root
。
在进一步遍历之前,React Fiber 创建根fiber。每棵 Fiber 树都有一个根节点。在我们的例子中,它是
HostRoot
。如果我们在 DOM 中导入多个 React 应用程序,则可以有多个根节点。第一次渲染之前,没有任何树产生。 React Fiber 遍历每个组件的渲染函数的输出,并在树中为每个 React 元素创建一个 Fiber 节点。它使用
createFiberFromTypeAndProps
将 React 元素转换为 Fiber。React 元素可以是类组件或host(宿主)组件,例如
div
或span
。对于类组件,它创建一个实例,对于host组件,它从 React Element 获取数据/属性。因此,如示例所示,它创建了一个 Fiber。更进一步,它又创建了一个fiber W,然后它转到子 div 并创建了一个fiber L。
依此类推,它继续为其子元素创建了一个fiber LA 和 fiber LB。
fiber LA 的返回到的fiber为 fiber L(L也可称为父fiber)。
所以,这就是最终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,因为没有任何子节点,并且performUnitOfWor
k 函数调用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 的更多信息。
参考:
End.