阅读 243

【翻译】React Fiber 架构

本文是《React Fiber Architecture》这篇优秀文章的中文翻译版本,目的是方便英语不那么好的同学无障碍阅读

介绍

React Fiber 是对 React 核心算法的持续性重实现。这是 React 团队超过两年时间研究的结晶。

React Fiber 的目标是提高其对如动画、布局和手势等领域的适用性。它的标题功能是增量渲染:即将渲染任务分割成多个块并分布到多个帧中的能力。

关于本文

Fiber 引入一些新颖的概念,这些概念很难仅通过查看源码来理解。本文的开始是我在 React 项目中跟随 Fiber 的实现所记录笔记的集合。随着它的发展,我意识到它对其他人也可能是有用的资源。

我将尽可能地尝试使用最直白的语言,并通过明确定义关键的术语来避免使用术语。我还将尽可能地大量链接到外部资源。

请注意,我不在 React 团队中,发言也没有任何权威。本文不是官方的文档。我已经要求 React 团队的成员对它的准确性进行审核。

这也是一项正在进行中的工作。Fiber 是一个持续性项目,在它完成前将可能经历重大的重构。我也尝试将它的设计在这里文档化。非常欢迎大家提出改进和建议。

我的目标是在阅读本文后,你将对 Fiber 有足够的了解,在实践中紧随其后,甚至能在背后为 React 做出贡献。

先决条件

我强烈建议你在继续阅读前熟悉以下资源:

  • React 组件,元素,以及实例:组件通常是一个重载术语。牢牢掌握这些术语至关重要。
  • 协调:对 React 一致性比较算法的高级描述。
  • React 的基本理论概念:对没有实现负担的 React。 概念模型的描述。在初读时,它的其中一些可能没有意义。没关系,随着时间推移它将变得越来越有意义。
  • React 的设计原则:特别注重调度部分。它很好地解释了 React Fiber 的原因。

回顾

如果还没有准备好,请查看先决条件部分。 在深入新事物前,让我们回顾一些概念。

什么是协调

协调

React 使用该算法将一棵树与另一棵树比较来确定哪部分需要被更改。

更新

用于渲染 React 应用程序的数据更改。通常是 setState 的结果。最终导致重新渲染。

React API 的中心思想是对更新的考虑,就像它们导致整个应用程序重新渲染一样。这允许开发人员进行声明式推理,而不必担心如何有效地将应用程序从任何特定状态转换为另一种状态(A 到 B,B 到 C,C 到 A,以此类推)。

实际上,每次更改时重新渲染整个应用程序仅适用于最琐碎的应用程序;在真实世界中的应用程序,就性能而言,它的成本高的惊人。React 具有在保持高性能的同时创建整个应用程序重新渲染外观的优化。这些优化的体积是协调过程的一部分。

协调是通常被理解为虚拟 DOM 背后的算法。一个高级的描述是这样的:当你渲染一个 React 应用程序时,将生成描述这个应用程序的节点树并保存在内存中。然后将这棵树应用到渲染环境中,例如,对于浏览器应用程序,将其转换为一组 DOM 操作。当应用程序被更新(通常通过 setState),将生成一棵新的树。新的树与前一棵树进行比较来计算更新渲染应用程序所需要的操作。

尽管 Fiber 是重写协调器的基础,但 React 文档中描述的高级算法将大面积相同。关键点是:

  • 假定不同的组件类型生成实质上不同的树。React 将不会尝试去比较它们,而是完全替换旧的树。
  • 列表的比较是使用键进行的,键应是“稳定、可预测且唯一的”。

协调与渲染

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

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

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

Fiber 重实现了协调器。尽管渲染器将需要更改来支持(并利用)新的体系结构,但它主要与渲染无关。

调度

调度确定何时应执行任务的过程。

任务

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

React 的 Design Principles 文档在这个主题上非常出色,我在这里仅引用一下:

在当前实现中,React 递归地遍历树,并在一个单一 tick 中调用整个更新后的树的 render 函数。但是,将来可能会开始延迟一些更新来避免丢帧。

这是 React 设计中的常见主题。一些流行的库实现了“push”的方法,该方法在有可用的新数据时执行计算。但是,React 坚持使用“pull”的方法,在这种方法中,可以将计算延迟到必要的时候。

React 不是通用的数据处理库。它是用于构建用户界面的库。我们认为,它在一个应用程序中的唯一定位是知道哪些计算是现在有关的,哪些不是。

如果屏幕外有东西,我们可以延迟与此有关的任何逻辑。如果数据到达速度快于帧速率,我们可以合并和批量更新。我们可以将用户交互(例如由按钮点击引起的动画)的任务优先于次要的后台任务(例如渲染刚从网络加载的新内容),以避免丢帧。

关键点是:

  • 在用户界面中,无需立即应用每一个更新;事实上,这样做是浪费的,会导致丢帧并降低用户体验。
  • 不同类型的更新具有不同的优先级:动画更新需要比数据存储中的更新更快地完成。
  • 基于 push 的方法要求应用程序(你,程序员)决定如何安排任务。基于 pull 的方法使框架(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 的 type 描述了它所对应的组件。对于复合组件,type 是函数或 class 组件本身。对于原始组件(div,span 等等),type 为字符串。

从理论上来讲,type 为函数(如 v = f(d)),其执行由堆栈帧追踪。

与 type 相随,key 在协同期间被用于确定 Fiber 是否可以被重复使用。

child 和 sibling

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

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

function Parent() {
  return <Child />
}
复制代码

Parent 的 child Fiber 对应于 Child。

sibling 字段说明了 render 返回多个 child 的情况(Fiber 中的一项新特性!):

function Parent() {
  return [<Child1 />, <Child2 />]
}
复制代码

child Fiber 形成一个单链列表,其顶部是第一个 child。因此,在此示例中,Parent 的 child 是 Child1,Child1 的 sibling 是 Child2。

return

return Fiber 是程序在处理完当前 Fiber 之后应返回的 Fiber。从理论上来讲,它与堆栈帧返回的地址相同。也可以将它视为 parent Fiber。

如果 Fiber 具有多个 child Fiber,每一个 child Fiber 的 return Fiber 都是 parent Fiber。因此,在上一节我们的示例中,Child1 和 Child2 的 return Fiber 为 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

flush fiber 是将其输出渲染到屏幕上。

work-in-progress

尚未完成的 Fiber; 从理论上来讲, 是指尚未返回的堆栈帧。

在任何时候,一个组件实例最多具有两个与其对应的 Fiber:current Fiber,flushed Fiber,以及 work-in-progress Fiber。

current Fiber 的替代项是 work-in-progress Fiber,work-in-progress Fiber 的替代项是 current Fiber。

使用称为 cloneFiber 的函数延迟创建 Fiber 的替代项。并非总是创建一个新的对象,cloneFiber 将尝试重用 Fiber 的备用项(如果存在),以最大程度地减少分配。

你应该将 alternate 字段视为实现细节,但是它经常在源码中出现,因此在此处进行讨论很有价值。

output

原始组件

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

从理论上来讲,Fiber 的输出是函数的返回值。

每个 Fiber 最终都具有输出,但是输出仅在原始组件的叶节点上创建。然后将输出传递到树上。

输出是指最终提供给渲染器的输出,所以它可以将更改刷新到渲染环境。定义输出的创建和更新方式是渲染器的责任。

以后的章节

目前仅此而已,但是本文的完成度还远远不够。以后的章节将描述在更新整个生命周期中使用的算法。涵盖的主题包括:

  • 调度程序如何找到要执行的下一个任务单元。
  • 如何通过 Fiber 树追踪和传播优先级。
  • 调度程序如何知道何时暂停和继续任务。
  • 如何刷新任务并将其标记为完成。
  • 副作用(例如生命周期方法)如何工作。
  • 协程是什么,以及如何用于实现如上下文和布局等功能。

相关视频

关注下面的标签,发现更多相似文章
评论