前言
React Fiber对React核心算法的重构,React团队两年多研究的结果。 React Fiber的目标是增加对动画,布局和手势的实用性。最重要的功能就是增量渲染:能够将渲染工作分割成块并且分片执行。 其他的主要功能包括能够在新的更新进来的时候,暂停、中断或者重复执行某些工作;为不同类型的更新分配优先级;以及并发处理的新方法
关于文章
Fiber引入了几个新概念,仅通过查看react源码难以理解。一开始本文档用来记录我在react中实现fiber的笔记集合,随着它发展,我意识到对于其他人这也是有用的资源。
我将尝试使用最简单的语言,并通过明确定义关键术语来避免使用行业术语,在可能的情况下,我还会大量链接到外部资源。
需要注意的是,我不是react team的成员也不是任何权威发言人,这篇文章也不是react官方文档。我已经要求react团队成员审查文档的准确性。
文档本身也是一项持续的工作。Fiber是一个正在进行的项目,在完成之前可能会经历重大的重构。同时,我也在尝试在这里记录它的设计。我们非常欢迎对本文档提出改进和建议。
我的目标是,在阅读本文档后,您将很好地理解Fiber,能够跟进它的实现过程,甚至是回馈react。
预备知识
在继续阅读之前,强烈建议先阅读以下文章:
- React Components, Elements, and Instances
- Reconciliationreact调度算法的描述
- React Basic Theoretical ConceptsReact概念模型的描述,无关实现。
- React Design Principles这一部分解释了为什么要创造react fiber
回顾
在我们深入研究新内容之前,让我们回顾几个概念。
什么是调度?
reconciliation
React算法用于比较新旧dom树,以确定哪些部分需要更改。
update
react应用中数据发生变化,通常是setState引起的,最终导致re-render
React的API的核心思想是将更新视为导致整个应用程序重新渲染。这允许开发人员以声明方式进行推理,而不是担心如何有效地将应用程序从任何特定状态转换到另一个状态(A到B,B到C,C到A等)。即状态对应视图。
实际上,每次更改时重新渲染整个应用仅适用于最简单应用; 在现实中,它在性能方面成本过高。react在重新渲染整个视图时保持卓越性能,这些优化是调度的一部分。
reconciliation是普遍理解的“虚拟DOM”背后的算法,概括为:渲染React应用时,会生成描述应用的节点树并将其保存在内存中,然后将该树刷新到渲染环境 - 例如,在浏览器,它被转换为一组DOM操作。应用(通常通过setState)时,会生成一个新树,新树与前一个树进行比较,以计算出更新操作。
虽然fiber是自下而上的重写调度过程,但是顶层算法大体上一致,关键点没有改变:
- 假设不同类型的component生成不同的子树,react不会比较他们,而是创建新树替代。
- 比较列表时使用key,保证key的稳定的、可预测的且是唯一的。
Reconciliation versus rendering
Dom只是React渲染环境中的一个,其他主要渲染目标是通过react-native渲染原生的ios和Android视图。
React将渲染和调度分成不同阶段的工作,所以可以支持多个渲染目标。reconciler计算以确定树的那一部分发生改变,然后renderer利用这些信息更新app视图。
这种分离意味着react dom和react native可以使用自己的渲染器同时共享一个调度器。
fiber重新实现了reconciler。它不涉及渲染,但是渲染器需要作出改进来支持新的架构。
Scheduling
scheduling
确定何时执work的过程。
work
必须执行的计算。工作通常是更新(如setstate)的结果。
React的设计原则很好的说明了这个主题,引文如下:
在当前实现中,React以递归方式遍历树,并在单个tick中调用render函数更新树。 但是在将来它可能会延迟一些更新以避免丢帧。
一些流行的库实现了数据“推送”方法,其中在新数据可用时执行计算。 然而,React坚持“拉”数据方法,在这种方法中计算可以延迟到必要时。
想象一下promise(push)和yeild(pull)就能理解数据的推送和拉取--译者注
React不是通用的数据处理库, 它是用于构建用户界面的库。我们认为它在应用程序中具有独特的位置,可以知道哪些计算有及时价值的,哪些不是。
如果某些东西在屏幕外,我们可以延迟任何与之相关的逻辑。 如果数据的到达速度快于帧速率,我们可以合并并批量更新。
我们可以优先考虑来自用户交互(例如由按钮点击引起的动画)的工作,再处理其他工作(例如渲染刚刚从网络加载的新内容)以避免丢帧。
这其中的关键点:
- 在UI中,没有必要立即处理每个更新; 实际上,这样做可能会浪费,导致帧丢失并降低用户体验。
- 不同类型的更新具有不同的优先级 - 动画更新需要比例如来自数据存储的更新更快地完成。
- 基于推送的方法需要应用(程序员)决定如何安排工作。 基于拉取的方法允许框架(React)聪明的做出决策。
改造React的核心算法以利用调度是Fiber背后的驱动理念。
现在我们已经准备好深入Fiber的实现。 下一节将进入更具技术性部分。在继续之前,请确保您消化了以上讨论的内容。
什么是fiber?
我们即将讨论React Fiber架构的核心,是一种比开发人员通常想到的更低级别的抽象。 如果您发现自己在理解它时感到困难,请不要气馁。多试几次
我们已经确定,Fiber的主要目标是使React能够利用调度。 具体来说,需要能够
- 暂停工作,稍后再执行。
- 为不同类型的工作分配优先权
- 重复利用已经完成的工作
- 终止不需要的工作
为了做到这一点,我们首先需要一种方法将工作分解为更小的单元。 从某种意义上说,这就是fiber, 代表一种工作单元。
更进一步,让我们回到React组件的概念-数据的函数,通常表示为
v = f(d)
React应用的render函数类似调用某个方法,其body包含其他render函数的调用,以此类推。在考虑fiber时这个类推很重要
计算机通常跟踪程序执行的方式是使用调用栈。 执行函数时,会向栈添加新的栈帧。 该栈帧表示该函数执行的工作。
在处理UI时,问题在于如果同时执行太多工作,可能导致动画丢帧并且不稳定。此外,其中一些工作可能是不必要的,如果它被新的更新所取代。这就是ui组件和函数之间的差异所在,组件比一般的函数具有更多的关注点。
较新的浏览器(和React Native)实现了有助于解决这个问题的API:requestIdleCallback在空闲期间调用的低优先级函数,requestAnimationFrame在下一个动画帧上调用的高优先级函数。 问题是,为了使用这些API,您需要一种方法将渲染工作分解为增量单元。 如果仅依赖于调用堆栈,它将继续工作直到堆栈为空。
如果我们可以自定义调用堆栈的行为以优化渲染UI,那不是很好吗? 如果我们可以随意中断调用堆栈并手动操作堆栈帧,那不是很好吗?
这就是React Fiber的目的。 fiber是堆栈的重新实现,专门用于React组件。 您可以将fiber视为虚拟堆栈帧。
重新实现堆栈的优点是,您可以将堆栈帧保留在内存中,然后执行它们(无论何时)。 这对于实现我们的调度目标至关重要。
除了调度之外,手动处理堆栈帧还可以解锁并发和错误边界等功能。 我们将在以后的部分中介绍这些主题。
在下一节中,我们将更多地关注fiber的结构
fiber的结构
注意:随着我们深入实现细节部分,某些内容可能会发生变化的可能性会增加。 如果您发现任何错误或过时信息,请提交PR。
具体而言,fiber是一个JavaScript对象,它包含有关组件输入和输出信息。
fiber对应于堆栈帧,但它也对应于组件的实例。
以下是一些重要字段 (非所有)
type and key
fiber的type、key与React元素的用途相同。 (实际上,当从元素创建fiber时,会直接复制这两个字段。)
type描述了fiber对应组件的类型。对于composite组件type是function或class,对于host 组件(div,span,etc.),type是string
从概念上讲,类型是函数f(如在v=f(d)中),其执行由栈帧跟踪。
key用来决定fiber是否可以复用。
child and sibling
这些字段指向其他fiber,描述fiber树的结构
child fiber对应组件render方法的返回值,
function Parent() {
return <Child />
}
例子中Parent组件的fiber child属性指向Child组件的fiber。
sibling字段说明了render返回多个子项的情况(fiber中的一个新功能!)
function Parent() {
return [<Child1 />, <Child2 />]
}
子节点的fiber构成一个单链表结构,head指向第一个子节点的fiber。例子中,Parent的child指向Child1,Child1的sibling指向Child2
回到我们的功能类比,可以将child fiber视为尾调用函数。
return
return 属性指向处理完当前fiber应返回的fiber。在概念上等同于函数执行完返回执行栈。也可以认为是它的父级fiber
如果fiber具有多个子fiber,则每个子fiber的返回这个父fiber。 因此,在上一节的示例中,Child1和Child2的return是Parent fiber。
pendingProps and memoizedProps
从概念上讲,props是函数的参数。pendingProps在开始执行是设置,memoizedProps在执行完成时设置
当pendingProps和memoizedProps相同时,表示当前fiber可以重复利用,避免不需要的工作。
pendingWorkPriority
fiber所代表的工作的优先级的数字。 ReactPriorityLevel模块列出了不同的优先级及其代表的含义。
除NoWork(0)外,数字越大表示优先级越低。 例如,您可以使用以下函数检查fiber的优先级高于或等于给定的优先级:
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
调度程序使用优先级字段来搜索要执行的下一个工作单元。 该算法将在以后的部分中讨论。
alternate
flush
将fiber的output渲染到屏幕上
work-in-progress
还没有完成的fiber,类比还没有返回的桢栈。
在任何时候,一个组件最多具有两个与之对应的fiber。当前已经flush到屏幕的、尚未完成的。
使用cloneFiber方法可以惰性创建fiber alternate。不同于创建一个新的对象,cloneFiber总是尽可能的利用已有的fiber alternate来避免内存分配
应该将alternate字段看成具体的实现细节,因为在代码库里经常出现,在此讨论是有必要的。
output
host component
React应用中的叶子节点。特定于渲染环境(在浏览器中是div、span等标签)
从概念上讲,fiber的输出是函数的返回值 每个fiber最终都有输出,但输出只能由host组件在叶节点上创建,然后输出被传输到树上。
将来的章节
- 调度程序如何查找要执行的下一个工作单元
- 如何通过fiber树跟踪和传播优先级
- 调度程序如何知道何时暂停和恢复工作
- 如何刷新工作并标记为完成
- 副作用(如生命周期方法)如何工作
- 协程是什么,以及如何使用它来实现诸如上下文和布局之类的特性