介绍
React Fiber是对React核心算法的不断重新实现。 这是React团队经过两年多研究的结晶。
React Fiber的目标是提高其对动画,布局和手势等区域的适用性。 它的标题功能是增量渲染:将渲染工作分成多个块并将其分布到多个帧中的能力。
其他关键功能包括随着新更新(updates)的出现而暂停,中止或重用工作(work)的功能; 为不同类型的更新分配优先级的能力; 和新的并发原语。
关于本文件
Fiber引入了一些新颖的概念,仅通过查看代码难以理解。该文档起初是我在跟随React项目中Fiber的实现时所记笔记的集合。随着它的发展,我意识到它对于其他人也可能是有用的资源。
我将尝试使用最简单的语言,并通过明确定义关键术语来避免使用术语。如果可能,我还将大量链接到外部资源。
请注意,我不在React团队中,也没有任何权威人士的发言。这不是官方文档。我已经请求React团队的成员对其准确性进行审查。
这也是一项正在进行中的工作。Fiber是一个正在进行的项目,在完成之前也可能会进行重大重构。我也在这里尝试记录其设计。非常欢迎提出改进和建议。
我的目标是阅读本文档后,您将对Fiber有了足够的了解,可以在实现过程中紧跟其后
,甚至最终能够为React做出贡献。
先决条件
我强烈建议您在继续之前熟悉以下资源:
- React Components, Elements, and Instances - “ Component”通常是一个重载术语。 牢牢掌握这些术语至关重要。
- Reconciliation - 对React的协调算法的高级描述。
- React - Basic Theoretical Concepts - 没有实现的负担的React概念模型的描述。 在初读时,其中一些可能没有意义。 没关系,随着时间的推移它将变得更加有意义。
- React Design Principles - 特别注意协调(schedule)部分。 它很好地解释了the why of React Fiber.。
回顾
如果还没有了解先决条件,请查看先决条件部分。
在深入研究新事物之前,让我们回顾一些概念。
什么是协调(reconciliation)?
协调(reconciliation)
React使用该算法将一棵树与另一棵树进行比较,以确定需要更改的部分。
更新(update)
用于呈现React应用程序的数据中的更改。通常是setState的结果。最终导致重新渲染。
React API的中心思想是考虑更新,就像它们导致整个应用程序重新呈现一样。这使开发人员可以进行声明式推理,而不必担心如何有效地将应用程序从任何特定状态转换为另一种状态(A到B,B到C,C到A,依此类推)。
实际上,每次更改时重新渲染整个应用程序仅适用于最琐碎的应用程序;在现实世界的应用中,就性能而言,它的成本高得惊人。 React具有优化功能,可在保持出色性能的同时创建整个应用程序重新渲染的外观。这些优化的大部分是协调过程的一部分。
协调通常被称为“虚拟DOM”(virtual DOM)背后的算法。一个高级描述是这样的:渲染React应用程序时,将描述该应用程序的节点树生成并保存在内存中,然后将该树刷新到渲染环境中。例如,对于浏览器应用程序,将其转换为一组DOM操作。更新应用程序后(通常通过setState),将生成一棵新树。新树与前一棵树进行比较,以计算更新应用程序界面所需的操作。
尽管Fiber是协调器的基础重写,但与React文档中描述的高级算法将基本相同。关键点是:
- 假定不同的组件类型生成实质上不同的树。 React不会尝试区分它们,而是完全替换旧树。
- 列表的区分是使用键进行的。密钥应“稳定,可预测且唯一”。
协调与渲染
DOM只是React可以渲染的渲染环境之一,其他主要目标是通过React Native的本地iOS和Android视图。 (这就是为什么“虚拟DOM”有点用词不当的原因。)
它可以支持这么多目标的原因是因为React被设计为协调和渲染是两个独立的阶段。 协调器负责计算树的哪些部分已更改; 然后,渲染器使用该信息来实际更新渲染的应用程序。
这种分离意味着React DOM和React Native可以分别使用自己的渲染器,同时共享由React core提供的相同协调器。
Fiber重新实现了协调器, 尽管渲染器需要更改以支持(并利用)新体系结构,但它原则上与渲染无关。
调度(Scheduling)
调度(scheduling)
确定何时应执行工作的过程。
工作(work)
必须执行的任何计算。工作通常是更新的结果(例如setState)。
React的设计原则文档关于这个问题描述的非常好,我引用在下面:
在当前的实现中,React递归地遍历树,并在一个时间片中调用整个更新后的树的render函数。但是,将来可能会开始延迟一些更新,以避免丢帧。
这是React设计中的常见主题。一些流行的库实现了“推送”方法,该方法在有新数据可用时执行计算。但是,React坚持使用“拉”方法,在这种方法中,可以将计算延迟到必要的时候。
React不是通用的数据处理库。它是用于构建用户界面的库。我们认为,它处于应用程序中的地位是,以了解哪些计算现在相关,哪些不相关。
如果屏幕外有东西,我们可以延迟与此有关的任何逻辑。如果数据到达比帧速率更快,我们可以合并和批量更新。我们可以将用户交互(例如,由按钮单击引起的动画)的工作优先于次要的后台工作(例如,渲染刚从网络加载的新内容),以避免丢帧。
关键点是:
- 在用户界面中,无需立即应用每个更新。 实际上,这样做可能是浪费的,导致帧下降并降低用户体验。
- 不同类型的更新具有不同的优先级-动画更新需要比数据存储中的更新更快地完成。
- 基于推送的方法要求应用程序(您,程序员)决定如何安排工作。 基于拉的方法使框架(React)变得智能,并为您做出那些决定。
当前,React并没有充分利用调度的优势。 更新导致立即重新渲染整个子树。 彻底修改React的核心算法以利用调度是Fiber背后的驱动思想。
现在,我们准备深入研究Fiber的实现。 下一节比到目前为止我们讨论的内容更具技术性。 在继续之前,请确保您对以前的材料感到满意。
什么是Fiber?
我们将讨论React Fiber体系结构的核心。 Fibers是比应用程序开发人员通常想到的低得多的抽象。 如果您在尝试理解它时感到沮丧,请不要灰心。 继续尝试,最终将变得有意义。 (当您最终了解它时,请提出改进本节的建议。)
我们开始吧!
我们已经确定,Fiber的主要目标是使React能够利用调度(scheduling)的优势。 具体来说,我们需要能够
- 暂停工作,稍后再回来。
- 为不同类型的工作分配优先级。
- 重用以前完成的工作。
- 如果不再需要,则中止工作。
为了做到这一点,我们首先需要一种将工作分解为单元的方法。 从某种意义上讲,这就是Fiber。 Fiber代表工作单位。
更进一步,让我们回到React组件作为数据的函数(React components as functions of data)的概念,通常表示为
v = f(d)
因此,呈现React应用程序类似于调用一个函数,该函数的主体包含对其他函数的调用,依此类推。 当考虑Fiber时,这种类比很有用。
计算机通常使用调用栈来跟踪程序执行的方式。执行功能时,新的栈帧将添加到栈中。该栈帧表示该函数执行的工作。
在处理UI时,问题在于如果一次执行太多工作,可能会导致动画掉帧并显得断断续续。而且,如果最新的更新取代了某些工作,则这些工作可能是不必要的。这是UI组件和函数不一致的地方,因为与一般函数相比,组件具有更多特定的关注点。
较新的浏览器(和React Native)实现了有助于解决此确切问题的API:requestIdleCallback安排在空闲期间调用的低优先级函数,而requestAnimationFrame安排在下一个动画帧上调用的高优先级函数。问题在于,为了使用这些API,您需要一种将渲染工作分解为增量单位的方法。如果仅依赖调用栈,它将一直工作直到栈为空。
如果我们可以自定义调用栈的行为来优化呈现UI,那不是很好吗?如果我们可以随意中断调用栈并手动操作栈的帧,那不是很好吗?
这就是React Fiber的目的。Fiber是栈的重新实现,专门用于React组件。您可以将单个Fiber视为虚拟栈帧。
重新实现栈的优点是,您可以将栈帧保留在内存中,并根据需要(以及在任何时候)执行它们。这对于实现我们计划的目标至关重要。
除了调度之外,手动处理栈帧还可以释放并发和错误边界等功能。我们将在以后的章节中介绍这些主题。
在下一节中,我们将更多地研究Fiber的结构。
Fiber的结构
Note: 随着我们对实现细节的更加具体化,某些事情可能发生变化的可能性增加了。 如果发现任何错误或过时的信息,请提交PR。
具体而言,Fiber是JavaScript对象,其中包含组件信息,它的输入,和它的输出。
光纤对应于栈的帧,也对应于组件的实例。
这是属于Fiber的一些重要方面。 (此列表并不详尽。)
-
type
和key
Fiber的类型和键的作用与React元素的作用相同。 (实际上,当从元素创建Fiber时,这两个字段将直接复制。)
Fiber的类型描述了它所对应的组件。 对于复合组件,类型是函数或类组件本身。 对于host组件(div
,span
等),类型为字符串。
从概念上讲,类型是函数(如v = f(d)
),其执行由栈帧跟踪。
与类型一起,key在协调期间用于确定Fiber是否可以重复使用。
-
child
和sibling
这些字段指向其他fibers,描述了fiber的递归树结构。
子fiber对应于组件的render
方法返回的值。 所以在下面的例子中
function Parent() {
return <Child />
}
Parent
的子级fiber对应于Child
。
sibling
字段说明了render
返回多个子项的情况(Fiber中的一项新功能!):
function Parent() {
return [<Child1 />, <Child2 />]
}
子fiber形成一个单向链表,其头是第一个子组件。 因此,在此示例中,Parent
的孩子是Child1
,Child1
的兄弟姐妹是Child2
。
回到我们的函数类比,您可以将子fiber视为尾调用函数
。
-
return
返回fiber是程序在处理完当前fiber之后应返回的fiber。 从概念上讲,它与栈帧的返回地址相同。 也可以将其视为父fiber。
如果fiber具有多个子fiber,则每个子fiber的返回fiber都是父fiber。 因此,在上一节的示例中,Child1
和Child2
的返回fiber为Parent
。
-
pendingProps
和memoizedProps
从概念上讲,属性是函数的参数。 fiber的pendingProps
在执行开始时设置,memoizedProps
在结束时设置。
当传入的pendingProps
等于memoizedProps
时,它表示可以重新使用fiber的先前输出,从而避免了不必要的工作。
-
pendingWorkPriority
(待处理的工作优先级)
一个数字,指示fiber代表的工作优先级。 ReactPriorityLevel模块列出了不同的优先级及其代表的含义。
除NoWork
为0外,数字越大表示优先级越低。 例如,您可以使用以下功能来检查光纤的优先级是否至少与给定级别一样高:
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
此功能仅用于说明; 它实际上不是React Fiber代码库的一部分。
调度程序使用优先级字段来搜索要执行的下一个工作单元。 该算法将在以后的部分中讨论。
-
alternate
(备用)
flush(冲洗)
冲洗fiber是将其输出渲染到屏幕上。
work-in-progress(工作正在进行中)
尚未完成的fiber; 从概念上讲,尚未返回的栈帧。
在任何时候,一个组件实例最多具有两个与其对应的光纤:当前fiber,冲洗fiber和工作正在进行中的fiber。
The alternate of the current fiber is the work-in-progress, and the alternate of the work-in-progress is the current fiber.
当前fiber的备用是工作正在进行中,而工作正在进行中备用是当前fiber。
使用称为cloneFiber
的函数延迟创建fiber的备用项。 并非总是创建一个新的对象,而是cloneFiber
将尝试重用fiber的备用对象(如果存在),以最大程度地减少分配。
您应该将alternate
字段视为实现细节,但是它经常在代码库中弹出,因此在此处进行讨论很有价值。
output
(输出)
host component(主机组件)
React应用程序的叶节点。 它们特定于渲染环境(例如,在浏览器应用中,它们是div
,span
等)。 在JSX中,它们使用小写标记名称表示。
从概念上讲,fiber的输出是函数的返回值。
每个fiber最终都有输出,但是输出仅由主机组件在叶节点上创建。 然后将输出传送到树上。
输出是最终提供给渲染器的输出,以便可以将更改刷新到渲染环境。 定义输出的创建和更新方式是渲染器的责任。
未来的部分
目前仅此而已,但是本文档还远远不够完整。 以后的部分将描述在更新的整个生命周期中使用的算法。 涵盖的主题包括:
- 调度程序如何找到要执行的下一个工作单元。
- 如何通过fiber树跟踪和传播优先级。
- 调度程序如何知道何时暂停和继续工作。
- 如何刷新工作并将其标记为完成。
- 副作用(例如生命周期方法)如何工作。
- 协程是什么,以及如何用于实现上下文和布局等功能。