likes
comments
collection
share

React Fiber -- React背后的算法

作者站长头像
站长
· 阅读数 48

在本文中,我们将了解React的核心算法React FiberReact Fiber是在React 16中引入的一种新的协调算法。您可能听说过React 15中的虚拟DOM。它是旧的协调算法(也称为堆栈协调器),因为它内部使用了堆栈。不同的渲染器(如DOMNativeAndroid视图)共享同一个协调器。因此,将其称为虚拟DOM可能会引起混淆。

原文链接:blog.logrocket.com/deep-dive-r…

那么,让我们直接开始,了解React Fiber是什么。

React Fiber -- React背后的算法

简介

React Fiber是旧的协调器的完全重写,称为Fiber协调器。该名称来自于它使用的"Fiber"数据结构,用于表示DOM树的节点。我们将在以下部分详细介绍Fiber的细节。

Fiber协调器的主要目标是增量渲染、更好或更平滑地渲染UI动画和手势,并对用户交互作出响应。该协调器还允许将工作分成多个块,并将渲染工作分布到多个帧中。它还可以为每个工作单元定义优先级,并暂停、重用和中止工作。

React的一些其他功能包括从渲染函数返回多个元素、改进的错误处理(我们可以使用componentDidCatch方法获取更清晰的错误信息)和门户(portals)。

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

在深入技术细节之前,我建议您熟悉以下术语,因为它们有助于理解React Fiber

先决条件

协调

正如官方React文档中所解释的,协调是比较两个DOM树的算法。当初始渲染UI时,React创建一个节点树。每个单独的节点表示一个React元素。它创建一个虚拟树(即虚拟DOM),它是已渲染的DOM树的副本。在来自UI的任何更新后,它递归地比较两个树中的每个节点。累积的更改然后传递给渲染器。

调度

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

调度可以基于时间或优先级。更新应该按照截止时间进行调度。高优先级的工作应该优先于低优先级的工作。

requestIdleCallback requestAnimationFrame会在下一个动画帧之前调度高优先级函数的执行。类似地,requestIdleCallback会在帧末的空闲时间调度低优先级或非关键的函数的执行。

这展示了requestIdleCallback的使用。lowPriorityWork是一个回调函数,将在帧末的空闲时间调用。

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

因此,现在我们可以继续了解fiber对象本身的结构,并了解React Fiber的工作原理。

Fiber的结构 Fiber(小写的'f')是一个简单的JavaScript对象,表示React元素或DOM树的节点。它是一项工作的单元。相比之下,FiberReact Fiber协调器。

下面的示例展示了一个简单的React组件,它在根div中进行渲染。

这是一个简单的组件,根据从组件状态获取的数据显示一个项目列表。(为了简化示例,我用两个列表项代替了数据的.map和迭代过程。)还有一个按钮和一个span,用于显示列表项的数量。

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

它会为每个单独的React元素创建一个fiber,就像上面的示例中那样。例如,对于具有"class wrapper"的div元素,它会创建一个名为W的fiber。然后,对于具有"class list"的<div>元素,它会创建一个名为L的fiber,依此类推。让我们为两个列表项命名为LA和LB的fiber

稍后我们将看到它是如何进行迭代的以及树的最终结构。虽然我们将其称为树,但React Fiber实际上创建了一个节点的链表,其中每个节点都是一个fiber。而且父节点、子节点和兄弟节点之间存在关系。React使用return字段指向父节点,在任何一个子fiber完成工作后,应该返回到父节点。所以,在上面的示例中,LA的return是L,兄弟节点是LB。

那么,这个fiber对象实际上是什么样子的呢?

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

React Fiber是如何工作的?

React Fiber的工作原理是通过创建和管理一个链表树来实现的,它在更新时对树进行操作。

在此之前,让我们先解释一下当前树(current tree)和工作中树(workInProgress tree),以及树的遍历过程。

当前树是指当前被刷新以渲染UI的树结构,它是用于渲染当前UI的树。每当发生更新时,Fiber会构建一个工作中树,该树是根据React元素的更新数据创建的。React在这个工作中树上执行相应的操作,并将其用于下一次渲染。一旦这个工作中树在UI上完成渲染,它就成为当前树。

下面我们将看到React Fiber是如何创建链表树以及在更新时做出响应的。

React Fiber -- React背后的算法 Fiber树的遍历过程如下:

开始:Fiber从最顶层的React元素开始遍历,并为其创建一个Fiber节点。 子节点:然后,它进入子元素,并为该元素创建一个Fiber节点。这个过程一直持续到达叶子元素为止。 兄弟节点:现在,它检查是否存在兄弟元素。如果有兄弟元素,它会遍历兄弟子树,直到兄弟元素的叶子节点。 返回:如果没有兄弟元素,则返回到父节点。 每个Fiber都有一个child属性(如果没有子节点则为null)、sibling属性和parent属性(正如你在前面的部分中看到的Fiber的结构)。这些属性在Fiber中充当链表的指针。

React Fiber -- React背后的算法 让我们使用相同的示例,为对应的React元素命名相应的Fiber。

首先,我们快速介绍一下挂载阶段,在该阶段创建树,然后我们将看到在任何更新后发生的详细逻辑。

初始渲染 App组件被渲染在具有id为root的根div中。

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

在首次渲染之前,不会有任何树存在。React Fiber遍历每个组件的render函数的输出,并为每个React元素在树中创建一个Fiber节点。它使用createFiberFromTypeAndProps将React元素转换为Fiber。React元素可以是类组件或宿主组件,如div或span。对于类组件,它创建一个实例,而对于宿主组件,则从React元素获取数据/props。

因此,如示例所示,它创建了一个名为App的Fiber。然后,它创建了另一个名为W的Fiber,然后进入子元素div,并创建了一个名为L的Fiber。依此类推,为其子元素创建了一个名为LA和LB的Fiber。Fiber LA的return(在这种情况下也可以称为父Fiber)是Fiber L,兄弟Fiber是LB。

这就是最终的Fiber树的样子。

React Fiber -- React背后的算法 这就是使用子节点、兄弟节点和返回节点指针来连接树节点的方式。

更新阶段

现在,让我们来看看第二种情况,即更新,比如通过setState触发的更新。

在此时,Fiber已经有了当前树。对于每次更新,它会构建一个工作中树(workInProgress tree)。它从根Fiber开始遍历树,直到叶子节点。与初始渲染阶段不同,它不会为每个React元素创建一个新的Fiber。它只是使用已存在的Fiber,并在更新阶段中将更新后元素的新数据/props合并到该Fiber中。

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

React Fiber将更新分为多个工作单元。它可以为每个工作单元分配优先级,并且可以在不需要时暂停、重用或中止工作单元。React Fiber将工作分解为多个工作单元,即Fiber。它将工作安排在多个帧中,并使用来自requestIdleCallback的截止时间。每个更新都有其定义的优先级,例如动画或用户输入的优先级高于从获取的数据中呈现项目列表的优先级。Fiber使用requestAnimationFrame来处理优先级较高的更新,并使用requestIdleCallback来处理优先级较低的更新。因此,在调度工作时,Fiber会检查当前更新的优先级和截止时间(帧结束后的空闲时间)。

如果优先级高于待处理的工作,或者没有截止时间或尚未到达截止时间,Fiber可以在单个帧后调度多个工作单元。下一组工作单元会在后续帧中继续执行。这就是Fiber能够暂停、重用和中止工作单元的原因。

因此,让我们看看在安排的工作中实际发生了什么。完成工作需要两个阶段:渲染(render)和提交(commit)。

渲染阶段(Render Phase)

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

我们可以将这个阶段称为协调(reconciliation)阶段。Fiber从Fiber树的根节点开始遍历并处理每个Fiber。对于每个工作单元,都会调用workLoop函数来执行工作。我们可以将工作的处理分为两个步骤:开始(begin)和完成(complete)。

开始步骤(Begin Step)

如果在React代码库中找到workLoop函数,它会调用performUnitOfWork,将nextUnitOfWork作为参数传递进去。nextUnitOfWork就是即将执行的工作单元。performUnitOfWork函数内部调用beginWork函数。这是Fiber上实际工作发生的地方,而performUnitOfWork函数则用于迭代。

beginWork函数内部,如果Fiber没有任何待处理的工作,它会直接跳过该Fiber而不进入开始阶段。这就是在遍历大型树时,Fiber如何跳过已处理的Fiber并直接跳到具有待处理工作的Fiber的方式。如果查看beginWork函数的大型代码块,会发现一个switch块,根据Fiber的标签调用相应的Fiber更新函数,比如对于host组件则调用updateHostComponent。这些函数会更新Fiber。

beginWork函数返回子Fiber(如果有子Fiber)或null(如果没有子Fiber)。performUnitOfWork函数会继续迭代并调用子Fiber,直到达到叶子节点。对于叶子节点,beginWork返回null,因为没有子节点,然后performUnitOfWork函数调用completeUnitOfWork函数。现在我们来看一下完成步骤。

完成步骤(Complete Step)

completeUnitOfWork函数通过调用completeWork函数来完成当前工作单元。completeUnitOfWork会返回兄弟Fiber(如果有)以执行下一个工作单元,否则会完成返回(父)Fiber的工作。这一过程会一直持续到返回值为null,即达到根节点。和beginWork类似,completeWork也是一个实际工作发生的函数,而completeUnitOfWork则用于迭代。

渲染阶段的结果会创建一个效果列表(副作用)。这些效果可以是插入、更新或删除主机组件的节点,或者调用类组件节点的生命周期方法。Fiber会用相应的效果标记各个Fiber。

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

提交阶段(Commit Phase)

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

在这个阶段的开始,Fiber有已经在UI上渲染的当前树,即finishedWork,或者在渲染阶段构建的workInProgress树以及效果列表。

效果列表是具有副作用的Fiber的链表。因此,它是渲染阶段的workInProgress树的一部分,其中包含了副作用(更新)。效果列表的节点使用nextEffect指针连接起来。

在这个阶段调用的函数是completeRoot

在这里,workInProgress树成为当前树,因为它用于渲染UI。实际的DOM更新,比如插入、更新、删除和生命周期方法的调用,或者与引用相关的更新,都会应用于效果列表中存在的节点。

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

结论

这就是React Fiber协调器如何将工作分解为多个工作单元的方式。它为每个工作单元设置优先级,并使得暂停、重用和中止工作单元成为可能。在Fiber树中,每个节点都跟踪了使上述操作成为可能的关系。每个Fiber都是通过子节点、兄弟节点和父节点引用连接起来的链表的节点。

以下是一些资源列表,您可以通过它们了解更多关于React Fiber的知识。

通过阅读这些资源,您可以深入了解React Fiber的内部工作原理和相关概念。

转载自:https://juejin.cn/post/7259311837462675493
评论
请登录