【译】React Fiber Architecture

3,334 阅读12分钟
原文链接: zhuanlan.zhihu.com

【译】React Fiber Architecture

原文:React Fiber Architecture

原作者: acdlite

翻译: TuyaFE - 冰河

介绍

React Fiber 是 React 核心算法的持续重新实现。它是React 团队超过两年的研究结果。

React Fiber 的目的是增强对类似动画、布局和手势操作这些场景的适应性。他的头个特性是渐进式渲染:将渲染工作拆分成块并将其分散到多个帧的能力。

其他主要特性包括在新更新进来时暂定、取消或者重用更新的能力;给不同更新分配优先级的能力;以及新的并发原函数。

关于

Fiber 介绍了几个难以通过查看代码理解的新概念。本文档一开始作为我在React项目中跟随Fiber实现的一系列笔记。随着它的发展,我意识到他也可能是对其他人有用的资源。

我将会尽可能尝试使用最朴素的语言,并通过明确定义关键术语来避免行话。我也会尽可能列出一些重要的外部资源。

请注意我并不属于React团队,同时我所说的话不能代表任何权威。这不是一篇官方文档。不过我已经要求React团队的成员审查它的准确性。

这仍然是正在进行中的工作。Fiber是一个进行中的项目并且在它完成之前会有重大的重构。这里我也尝试记录它的设计。我们非常欢迎改进和建议。

我的目标是当你读完这篇文档,你将会很好的理解它的实现,最终对React做出贡献。

先决条件

我强烈建议在开始之前你们对下列的文档有相当的理解:

检查

请核对先决条件章节如果你还没有准备好。

在我们深入研究新内容之前,让我们回顾几个概念。

什么是协调器

协调器

React作为决定一颗树与另一颗树比较决定需要更新哪些部分的算法。

更新

渲染React应用的数据的一次变化。通常是_setState_的结果。最终导致重新渲染。

React的API的核心思想是将更新视为导致整个应用重新渲染。这允许开发人员以声明方式进行推理,而不是担心如何有效地将应用程序从任何特定状态转换到另一个状态(A到B,B到C,C到A等)。

实际上,每次更改时重新渲染整个应用仅适用于最琐碎的应用; 在现实世界的应用中,它在性能方面成本过高。React具有优化功能,可以在保持卓越性能的同时创建整个应用程序重新呈现的外观。这些优化中的大部分是称为协调的过程的一部分。

协调是人们普遍理解为“虚拟DOM”背后的算法。高级描述如下所示:当您呈现React应用时,会生成描述应用的节点树并将其保存在内存中。然后将此树刷新到渲染环境 - 例如,在浏览器应用程序的情况下,它将转换为一组DOM操作。更新应用程序(通常是通过setState)时,会生成一个新树。新树与前一个树进行对比,以计算更新渲染应用所需的操作。

虽然Fiber是协调器的重写,但和React文档中描述的高级算法将基本相同。关键点是:

  • 假设不同的组件类型生成实质上不同的树。React不会尝试区分它们,而是完全替换旧树。
  • 使用key执行列表的区分。key应该“稳定,可预测且唯一”。

协调与渲染

DOM只是React的渲染环境之一,其他主要目标是通过React Native的原生iOS和Android视图。 (这就是为什么“虚拟 DOM”有点用词不当)

它可以支持这么多目标的原因是因为React的设计使得协调和渲染是分开的阶段。协调器负责计算树的哪些部分已经改变;然后渲染器使用该信息来实际更新渲染的应用。

这种分离意味着React DOM和React Native可以使用自己的渲染器,同时共享由React核心提供的相同协调器。

Fiber重新实现了协调。虽然渲染器需要更改以支持(并利用)新架构,但它并不主要关注渲染。

调度

调度

确定何时应该进行工作的过程。

work

任何必须执行的计算。work通常是更新的结果(例如setState)。

React的设计原则文档在这个主题上解释的非常好,我在这里直接引用它:

在当前实现中,React以递归方式遍历树,并在单个tick中调用整个更新树的呈现函数。但是在将来它可能会开始延迟一些更新以避免丢帧。
这是React设计中的一个共同主题。一些流行的库实现了“推送”方法,其中在新数据可用时执行计算。然而,React坚持“拉取”方法,在这种方法中计算可以延迟到必要时。
React不是通用数据处理库。它是一个用于构建用户界面的库。我们认为它在应用程序中具有独特的位置,可以知道哪些计算现在是相关的,哪些不是。
如果某些东西在屏幕外,我们可以延迟任何与之相关的逻辑。如果数据的到达速度快于帧速率,我们可以合并和批量更新。我们可以优先考虑来自用户交互(例如由按钮点击引起的动画)的工作,而不是不太重要的背景工作(例如渲染刚刚从网络加载的新内容)以避免丢帧。

关键点是:

  • 在UI中,不必立即应用每个更新;实际上,这样做可能会浪费,导致帧丢失并降低用户体验。
  • 不同类型的更新具有不同的优先级 - 动画更新需要比例如来自数据存储的更新更快地完成。
  • 基于推送的方法需要应用(程序员)来决定如何安排工作。基于拉取的方法允许框架(React)变得聪明并为您做出决策。

React目前没有着重利用调度;更新导致整个子树立即重新渲染。改造React的核心算法以利用调度是Fiber背后的驱动理念。

现在我们已经准备好深入了解Fiber的实现。下一节比我们到目前为止所讨论的更具技术性。在继续之前,请确保您对前面的材料感到舒适。

什么是一个Fiber

我们即将讨论React Fiber架构的核心。Fiber是一种比应用开发人员通常想到的更低级别的抽象。如果您发现自己在理解它时感到沮丧,请不要气馁。继续尝试,你最终会理解。 (当你最终理解时,请建议如何改进这一部分。)

我们已经确定,Fiber的主要目标是使React能够利用调度。具体来说,我们需要能够

  • 暂停工作,稍后回来
  • 给不同类型的工作分配优先级
  • 重用之前已经完成的工作
  • 当工作不再需要时取消

为了做到这一点,我们首先需要一种将工作分解为单元的方法。从某种意义上说,这就是Fiber。Fiber代表一种工作单位。

更进一步,让我们回到React组件的概念作为数据的功能,通常表达为

v = f(d)

因此,呈现React应用类似于调用其主体包含对其他函数的调用的函数,依此类推。在考虑Fiber时,这种类比很有用。

计算机通常跟踪程序执行的方式是使用调用堆栈。执行函数时,会将新的堆栈帧添加到堆栈中。该堆栈帧表示该功能执行的工作。

在处理UI时,问题在于如果同时执行太多工作,则可能导致动画丢帧并且看起来不稳定。更重要的是,如果它会被更新的更新取代,那么其中一些工作可能是不必要的。这是对比UI组件和函数之间不同的地方,因为组件比一般的函数具有更多特定的关注点。

较新的浏览器(和React Native)实现了有助于解决这个问题的API:requestIdleCallback调度在空闲期间调用的低优先级函数,并requestAnimationFrame调度在下一个动画帧上调用的高优先级函数。问题是,为了使用这些API,您需要一种方法将渲染工作分解为增量单元。如果仅依赖于调用堆栈,它将继续工作直到堆栈为空。

如果我们可以自定义调用堆栈的行为以优化渲染UI,那不是很好吗?如果我们可以随意中断调用堆栈并手动操作堆栈帧,那不是很好吗?

这就是React Fiber的目的。Fiber是堆栈的重新实现,专门用于React组件。您可以将单个Fiber视为虚拟堆栈帧。

重新实现堆栈的优点是,您可以将堆栈帧保留在内存中,然后执行它们(无论何时)。这对于实现我们的调度目标至关重要。

除了调度之外,手动处理堆栈帧还可以释放并发和错误边界等功能。我们将在以后的章节中介绍这些主题。

在下一节中,我们将更多地关注Fiber的结构。

Fiber的结构

注意:随着我们对实现细节的更具体了解,某些内容可能会发生变化的可能性会增加。如果您发现任何错误或过时信息,请提交PR。

具体而言,Fiber是一个JavaScript对象,它包含有关组件输入和输出的信息。

Fiber对应于堆栈帧,但它也对应于组件的实例。

以下是属于Fiber的一些重要字段。(此列表并不全面。)

type 和 key

Fiber的type和key与React元素的用途相同。(实际上,当从元素创建Fiber时,会直接复制这两个字段。)

Fiber的类型描述了它对应的组件。对于复合组件,类型是函数或类组件本身。对于宿主组件(div,span等),类型是字符串。

从概念上讲,type是函数(如在v = f(d))中,其执行由堆栈帧跟踪。

除了type之外,在协调器中使用key来确定Fiber是否可以重复使用。

child 和 sibling

这些字段指向其他Fiber,描述Fiber的递归树结构。

子Fiber对应于组件render方法返回的值。所以在下面的例子中

function  Parent(){
   return  < Child / > 
}

Parent的子Fiber对应Child。

兄弟字段解释了render返回多个子节点的情况(Fiber中的新功能!):

function Parent() {
  return [<Child1 />, <Child2 />]
}

子Fiber形成一个单链表,其头部是第一个孩子。所以在这个例子中,Parentis 的孩子Child1和Child1的兄弟是Child2。

回到我们的功能类比,您可以将子Fiber视为尾调用函数

return

返回fiber是程序在处理当前fiber后应返回的fiber。它在概念上与堆栈帧的返回地址相同。它也可以被认为是母fiber。

如果fiber具有多个子fiber,则每个子fiber的返回fiber是父fiber。所以,在我们的例子在上一节,返回纤维Child1和Child2是Parent。

pendingProps 和 memoizedProps

从概念上讲,props是函数的参数。一个 fiber 的pendingProps在执行开始时设置,并在结束时设置memoizedProps。

当输入的pendingProps等于memoizedProps时,它表示fiber的先前输出可以重复使用,从而防止不必要的工作。

pendingWorkPriority

一个数字,表示fiber所代表的工作的优先级。该ReactPriorityLevel模块列出了不同的优先级和它们代表什么。

除了NoWork(表示0)之外,较大的数字表示较低的优先级。例如,您可以使用以下函数来检查fiber的优先级是否至少与给定级别一样高:

function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &&
         fiber.pendingWorkPriority <= priority
}

此功能仅供参考; 它实际上并不是React Fiber代码库的一部分。

调度程序使用优先级字段来搜索要执行的下一个工作单元。该算法将在后面章节讨论。

alternate

/flush/

冲洗fiber是为了将其输出呈现在屏幕上。

/work-in-progress/

一种尚未完成的fiber; 从概念上讲,还没有返回的堆栈帧。

在任何时候,组件实例最多具有两个与之对应的fiber:当前的,flushed fiber和进行中的fiber。

当前fiber的替代是work-in-progress,并且work-in-progress的替代是当前fiber。

Fiber的替代是使用一个名为lazily的函数创建的cloneFiber。而不是总是创建一个新对象,如果存在,cloneFiber将尝试重用fiber的替代,以最小化分配。

你应该将该alternate字段视为一个实现细节,但它在代码库中经常出现,这里讨论它很有价值。

output

/host component/

React应用程序的叶节点。它们特定于渲染环境(例如,在浏览器应用程序中,它们是`div`,`span`等)。在JSX     中,它们使用小写标记名称表示。

从概念上讲,光纤的输出是函数的返回值。

每个fiber最终都有输出,但是主机组件仅在叶节点处创建输出。然后输出在树上传输。

输出是最终赋予渲染器的输出,以便它可以更改到渲染环境。渲染器负责定义输出的创建和更新方式。

未完待续…