原文: React Fiber Architecturehtml
React Fiber是React核心算法的持续再实现,这是React团队两年多研究的成果。react
React Fiber的目标是加强其在动画、布局和手势等领域的适用性。它的主要特性是增量渲染:可以将渲染工做分割成块,并将其分散到多个帧中。git
其余关键功能包括暂停、停止或重启更新工做的能力;为不一样类型的更新分配优先级的能力;以及新的并发单元。程序员
Fiber引入了一些新的概念,这些概念很难仅仅经过查看代码来理解。本文档一开始是我在关注React项目中Fiber的实现过程当中所作的笔记。随着它的发展,我意识到它可能对其余人也颇有帮助。github
我将尽可能使用最简单的语言,并经过明肯定义关键术语来避免术语。若是可能的话,我还将大量连接到外部资源。算法
请注意,我不是React团队的一员,算不上权威人士。这不是官方文档。 我已经请求React团队的成员检查它的准确性。c#
这也是一项正在进行中的工做。Fiber是一个正在进行的项目,在它完成以前可能会经历重大的重构。 我也会继续尝试在这里记录它的设计,欢迎提出改进和建议。api
个人目标是在阅读了本文以后,你将可以很好地理解Fiber,以便在其实现过程当中进行跟踪,并最终可以对其做出贡献。浏览器
在继续学习以前,我强烈建议你熟悉如下资源:网络
请检查预备知识部分的内容是否已经准备好。
在开始学习新内容以前,让咱们先复习一些概念。
协调算法
React使用该算法来比较两棵树之间的不一样,而后决定出须要改变的部分。
更新
用于渲染React应用程序的数据变化一般是setState
的结果,最终致使从新渲染。
React API的核心思想是将更新看做是致使整个应用程序从新渲染的缘由。这容许开发人员以声明的方式推理,而没必要担忧如何有效地将应用程序从一些特定状态转换到另外一个状态(A到B, B到C, C到A,等等)。
实际上,在每次更改时从新渲染整个应用程序只适用于最普通的应用;在真正的应用程序中,它所消耗的性能代价很是高。React对此进行了优化,在保持良好性能的同时从新渲染整个应用的界面。这些优化的大部份内容是一个被称为 协调 算法处理完成的。
协调是一种被广泛理解为“虚拟DOM”的算法。对其高层次的描述是这样的:当你渲染一个React应用,用于描述app的节点树被生成而且保存在内存中。而后这棵树会被放入到渲染环境——好比,在基于浏览器的应用中,它被转换成一组DOM操做。当app更新后(通常经过setState
),一个新的树就会随之产生。这颗新树与先前那棵树的不一样是它要计算出哪些操做须要更新到渲染app中。
尽管Fiber是对“协调算法”底层方面全新的重写,可是这个高级算法在React文档中描述大体相同。关键点在于:
diff
它们,而是将旧的树彻底替换。list
)之间的diff
使用它们keys
,由于keys
是“稳定的、可预测的和唯一的”。DOM只是React能够渲染的“渲染环境”之一,其余主要的目标是经过React native实现的iOS和Android视图。(这就是为何“虚拟DOM”有点用词不当。)
它之因此可以支持如此多的目标,是由于React被设计成协调和渲染是两个独立的阶段。协调器要作的工做是计算出一棵树的哪些部分发生了变化;而后渲染器使用这些信息来更新渲染app。
这种分离意味着React DOM和React Native可使用它们本身的渲染器,同时共享React core提供的同一个协调器。
Fiber从新实现了协调器。它变得渲染没有过高的相关性,所以渲染器也须要改变以支持(并利用)新的架构。
调度
肯定什么时候完成work
的过程。
work
一些必须执行的运算。Work
一般是更新的结果(例如setState
)。
React的 设计原则 文档在这方面作得很好,我在这里引用一下:
在其当前实现中,React递归遍历树,并在单个标记期间调用整个更新树的呈现函数。可是在未来,它可能会开始延迟一些更新以免丢失帧 (能够理解为交互卡顿)。
这是React设计中的一个常见主题。一些流行的框架实现了“
push
”方法,即在新数据可用时执行计算。然而,React坚持使用“pull
”方法,在这种方法中,计算能够延迟到必要的时候。
React不是通常的数据处理框架。它是一个用于构建用户界面的框架。咱们认为它在应用程序中处于独特的位置,能够知道哪些计算是相关的,哪些不是。
若是浏览器屏幕外发生了什么(好比
click
事件等),咱们能够延迟与之相关的一些逻辑。若是数据到达的速度快于帧速率,咱们能够将它们合并而后批量更新。咱们能够优先处理来自用户交互的工做(如单击按钮所引发的动画),而不是不过重要的后台工做(如渲染刚从网络加载的新内容),以免丢失帧 (能够理解为交互卡顿)。
重点是:
push
的方法须要应用程序(你,程序员)来决定如何安排工做。基于pull
的方法容许框架(React)为您作出明智的决策。React在此以前并无很好地利用调度,所以一个更新会当即从新渲染整个子树。今天,利用调度优点的React核心算法是Fiber背后的驱动思想。
如今,咱们准备深刻讨论Fiber的实现。下一节比咱们到目前为止讨论的内容更具备技术性。在继续以前,请确保你对前面的内容达到能够接受的状态。
咱们将讨论React Fiber的核心架构。相对于应用开发人员一般的认知,Fiber是一个至关底层的抽象。若是你发现本身在试图理解它的过程当中受挫,不要气馁。不断尝试,最终会搞定它的。(当你最终理解它后,请建议咱们如何更好的提升本章节的描述)。
咱们开始吧!
咱们已经为Fiber确立了一个主要的目标就是让React更好的利用调度。具体来讲,咱们须要可以:
work
定义优先级。为了作到这一点,咱们首先须要一种把work
分解成单位的方法。从某种意义上说,这就是Fiber。一根 光纤 表明一个工做单位。
为了更进一步,让咱们回到 React组件做为数据函数 的概念,一般表示为:
v = f(d)
复制代码
渲染React app比如调用一个函数,该函数的函数体包含了对其余函数的调用等等。这个类比在讨论Fiber时颇有用。
计算机跟踪程序执行的典型方式就是使用“堆栈”。当一个函数被执行,一个新的堆栈帧被添加到堆栈中。这个堆栈帧表明了函数所执行的那项工做。
在处理ui时,遇到的问题是若是一次性执行了太多的工做,就会致使动画丢失帧,看起来很不稳定。更重要的是,一些工做多是没必要要的,若是它被一个更近期的更新取代。这就是UI组件和函数之间的比较失败的地方,由于组件比通常的函数有更具体的关注点。
更新的浏览器(和React Native)实现的api能够帮助解决这个问题:requestIdleCallback
在空闲期间调度被调用的低优先级函数,requestAnimationFrame
在下一个动画帧调度被调用的高优先级函数。如今的问题是,为了使用这些api,你须要一种方法来将渲染工做分解为增量单元。若是你只依赖于调用堆栈,它将一直工做,直到堆栈为空。
若是咱们能够定制调用堆栈的行为来优化渲染ui,那岂不是更好?若是咱们能够随意中断调用堆栈并手动操做堆栈帧,那岂不是更好?
这就是React Fiber的目标。Fiber是栈的从新实现,专门用于React组件。你能够将单个Fiber视为一个虚拟堆栈帧。
从新实现堆栈的好处是,你能够 将堆栈帧保存在内存中 ,并在须要的时候以任何方式执行它们。这对于完成咱们的调度目标是相当重要的。
除了调度以外,手动处理堆栈帧还能够解决并发性和错误边界等潜在特性问题。咱们将在之后的章节中讨论这些问题。
在下一节中,咱们将更多地研究一个fiber的结构。
注意:随着咱们对实现细节的了解愈来愈具体,发生变化的可能性也愈来愈大。若是您注意到任何错误或过期的信息,请提交一份PR。
具体而言,fiber是一个JavaScript对象,它包含关于组件的输入和输出信息。
一个fiber对应了一个堆栈帧,也对应了一个组件的实例。
如下是fiber的一些属性。(这份清单并不详尽。)
type
和 key
fiber的type
和key
在React元素上面有着相同的做用。事实上,当从一个元素建立一个fiber时,这两个字段被直接复制。
一个fiber的类型描述了它所对应的组件。对于复合组件,类型是函数或类组件自己。对于宿主组件(div
、span
等),类型是字符串。
从概念上讲,类型是函数(如v = f(d)
)的组件,它的执行会由堆栈帧跟踪。
除了类型以外,在协调期间还使用key
来肯定是否能够重用fiber。
child
和 sibling
这两个字段指向了其余fiber节点,描述了fiber的递归树结构。
孩子(child)fiber节点对应着组件render
函数的返回值。因此在下面的例子中
function Parent() {
return <Child /> } 复制代码
组件Parent
的孩子fiber节点是Child
组件。
sibling
字段解释了渲染返回多个子元素的状况(这是Fiber的一个新特性!)。
function Parent() {
return [<Child1 />, <Child2 />] } 复制代码
孩子fiber节点造成了一个链表,链表的头部是第一个孩子fiber节点。在这个例子中,父结点的子结点是Child1
,而Child1
的兄弟结点是Child2
。
回到咱们的函数类比,你能够把孩子fiber想象成 尾部调用函数。
return
返回fiber节点是程序在处理当前fiber节点后应该返回的fiber节点。它在概念上与堆栈帧的返回地址相同。它也能够被认为是父fiber节点。
若是一个fiber节点有多个孩子fiber节点,每个孩子的返回fiber就是父fiber节点。在上一节的例子中,Child1
和Child2
的返回fiber节点是父结点。
pendingProps
和 memoizedProps
从概念上讲,属性是函数的参数。fiber的pendingProps
设置在执行的开始,memoizedProps
设置在执行的最后。
当输入的pendingProps
与memoizedProps
相等时,就表示fiber 以前的输出能够重复使用,避免了没必要要的工做。
在fiber中,用一个数字来表示work
的优先级。ReactPriorityLevel 模块列出了不一样的优先级和它们所表明的内容。
除了NoWork
是0以外,较大的数字表示较低的优先级。例如,您可使用如下函数来检查fiber节点的优先级是否至少与给定的级别相同:
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
复制代码
此函数仅供参考,它并非React Fiber代码的一部分。
调度器使用priority
字段搜索要执行的下一个工做单元。这个算法将在之后的章节中讨论。
flush(刷新)
刷新 fiber就是把它的输出渲染到屏幕上。
work-in-progress
一个fiber还没有完成工做,从概念上讲,就是还没有返回堆栈帧。
在任什么时候候,组件实例最多有两个与之对应的fiber树:当前的、已刷新的fiber和正在工做的fiber。
当前的替代fiber就是work-in-progress,work-in-progress的替代就是当前fiber。
fiber的替代是使用名为cloneFiber
的函数惰性建立的。与老是建立新对象不一样,cloneFiber
将尝试重用存在的替代对象,从而最小化分配。
你应该将alternate
字段视为实现细节,可是它在代码库中常常出现,所以有必要在这里讨论它。
host component
React应用程序的叶子节点。它们特定于呈现环境(例如,在浏览器应用程序中,它们是“div”
、“span”
等)。在JSX中,它们用小写标记名表示。
从概念上讲,fiber的输出是函数的返回值。
每一个fiber最终都有输出,但输出仅由 host components (宿主组件)在叶节点上建立。而后输出被传送到树上。
输出是最终给渲染器的,这样它就能够刷新更改到渲染环境。渲染器的职责是定义如何建立和更新输出。
这就是目前的所有内容,可是这个文档还远远没有完成。后面的部分将描述在更新的整个生命周期中使用的算法。主题包括:
work
是如何被刷新以及被标记以及完成的。