原文地址:The how and why on React’s usage of linked list in Fiber to walk the component’s treejavascript
React中,改变检测一般被看做是协调(reconciliation)或者渲染(rendering),而Fiber正是这个机制的一种新的实现。在这个架构之下,能够实现一些有趣特性,如:改善非阻塞渲染,执行基于优先级的更新,以及在后台提早渲染内容。这些特性在并发React哲学中被认为是time-slicing。除了解决开发者一些真实问题外,这些机制的内部实现,从工程角度来看也具备普遍的吸引力。关于其中原因的有价值的知识点将有助于咱们做为开发者获得成长。html
若是你如今Google查询“ReactFiber”,你会搜到大量的相关文章,这些除了Andrew Clark的笔记都是至关高层面的讲解。本篇文章中我将引用这个资源,而且提供一个细致的关于Fiber中一些特别重要概念的讲解。当咱们结束时,你将对Lin Clark在ReactConf 2017上关于工做方法(work loop)的演讲有足够的知识来理解,这个演讲你须要看一下,不过在你大体看完以后,我将让你有更多的理解。一篇解析一系列关于React Fiber内幕的文章,我大概花了70%的时间用来理解其内部的详细实现,过程当中还写了三篇关于协调和渲染机制的文章。java
咱们开始吧。node
Fiber的架构有两个主要阶段:协调/渲染(reconciliation/render)和提交(commit)。在源码中协调阶段基本上被看成是“渲染阶段(render phase)”。这个阶段中,React会遍历组件树而且会:react
全部这些操做在Fiber中被认为是一个work。须要操做的work的类型取决于React Element的类型,例如,对于一个Class Component
React须要实例化一个类,但对于Function Component
则不须要。若是感兴趣,你能够在这里看到Fiber中全部work对象的类型。这些操做确实如Andrew演讲中所提到的:git
当处理一些UI时,有一个问题是,若是一次性执行太多的操做,那么将会致使动画掉帧github
那“一次性”指的是什么呢?通常来讲,React会同步遍历整个组件树,而且执行每一个组件的work,而执行它逻辑的时间可能超出了16ms。这便致使之了掉帧,继而引发视图卡顿。web
那,这有什么办法能够解决吗?算法
现代浏览器(包括React Native)实现了一些API有助于解决这个问题数组
一个新的全局方法的API叫 requestIdleCallback 能够添加一些方法,而这些添加的方法将在浏览器闲置时间时被执行。你怎么能够本身使用一下呢?若是我在Chrome的console面板,执行如上代码,会打印出49.9和false。这表示我能够有49.9ms来执行想要作的事情,且我没有用完分配的时间,不然deadline.didTimeout
就是true了。记住,只要浏览器有一些工做须要作,那么timeRemaing
就会变化,须要不断地检测它。
requestIdleCallback确实有点使用限制,且不老是充分地执行来保证平滑的UI渲染,因此React团队必须实现本身的一个版本。
requestIdleCallback((deadline) => {
// while we have time, perform work for a part of the components tree
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
nextComponent = performWork(nextComponent);
}
});
复制代码
咱们在一个组件上执行work,而后返回下一个待继续执行组件的引用。若是不是只处理一件事的状况下,这种方式是有效的。你不能够同步处理整个组件树,就像以前React关于协调算法的实现。这就如Andrew演讲中所提到的问题:
为了使用这些APIs(即requestIdleCallback),你须要一种方式,将渲染方式(rendering work)打破成可递增的单元
因此为了解决这个问题,React必须得从新实现遍历组件树的方法:从依赖内建调用栈来同步递归模式,换成使用链表和指针的异步模式。这即是Andrew所写的:
若是你只是依赖内建的调用栈,那它将一直执行直到栈为空。若是咱们能够按照需求打断调用栈,并手动维护栈帧,这样不就最好了。这就是React Fiber的目的,Fiber则是特别针对React组件来从新实现的栈,你也能够认为一个fiber就是一个虚拟的栈帧。
这就是我如今讲解的内容。
假设你对调用栈的感念比较熟悉,当你在浏览器调试工具中断点时就能够看到它,这里是来自Wikipedia的引用和示例图:
在计算机科学中,一个调用栈是栈的数据结构,用于保存计算机程序中活跃子程序的信息。设计调用栈的主要缘由是为了跟踪每个活跃子程序的引用,以便子程序执行结束时能够返回控制权。一个调用栈是有一些栈帧组成的,每一个栈帧对应的就是每一个尚未结束的持有返回的子程序。例如,一个叫
DrawLine
的子程序正在执行,尚未被子程序DrawSquare
调用,那这个调用栈的顶层部分的构成就像以下图片所示。
正如这篇文章第一部分中所说,React在协调/渲染阶段遍历组件树,并在组件上执行一些操做,以前的协调算法是依赖内建调用栈的同步模式来遍历树。关于这个协调算法的官方文档 描述了这个过程,且谈及许多关于递归:
默认状况下,当递归DOM节点的子节点时,React会在同一时间遍历全部子节点列表,并由任什么时候间产生的一个diff计算出一个突变。
想想,每次递归调用会在栈上添加一帧,且这个过程是同步的。假设咱们有以下组件树:
以render
方法表示成一些对象,你能够把它当作组件的实例。
const a1 = {name: 'a1'};
const b1 = {name: 'b1'};
const b2 = {name: 'b2'};
const b3 = {name: 'b3'};
const c1 = {name: 'c1'};
const c2 = {name: 'c2'};
const d1 = {name: 'd1'};
const d2 = {name: 'd2'};
a1.render = () => [b1, b2, b3];
b1.render = () => [];
b2.render = () => [c1];
b3.render = () => [c2];
c1.render = () => [d1, d2];
c2.render = () => [];
d1.render = () => [];
d2.render = () => [];
复制代码
React须要遍历这棵树来执行一些组件上的操做,为了简单化,这个操做只是打印出当前组件的名字,且获取子组件。看我怎么用递归来作吧。
主要的遍历这颗树的方法叫作walk
,以下实现:
walk(a1);
function walk(instance) {
doWork(instance);
const children = instance.render();
children.forEach(walk);
}
function doWork(o) {
console.log(o.name);
}
复制代码
咱们获得的输出结果是:a1, b1, b2, c1, d1, d2, b3, c2
若是你不太明确递归,那请看我关于递归的深刻解析文章.
递归对于遍历树是一种比较直观且相对合适的方式。不过它也有一些限制,最大的一个即是不能将遍历过程打破成可递增的单元,咱们不能在某个特定的组件上中止操做,以后再继续。因此React使用这个方式就保持遍历直处处理完全部的组件以及递归栈为空。
那么,React如何不使用递归来遍历组件树的呢?它使用了单链表遍历树算法,这样就能够暂停遍历且阻止栈的增加。
我在这里找到Sebastian Markbåge关于该算法的大体说明。为了实现这个算法,咱们须要一个数据结构,包含三个字段:
在React新的协调算法环境中,这个数据结构叫作Fiber。在内部,她表示了一个保持队列工做的React节点,更多关于它的细节能够看我下一篇文章。
如下实例图示范了链表中连接对象组成结构,以及二者之间的关联方式:
那让咱们来定义咱们的定制的节点构造方法:
class Node {
constructor(instance) {
this.instance = instance;
this.child = null;
this.sibling = null;
this.return = null;
}
}
复制代码
以及一个接受节点数组而后将它们链表起来的方法,咱们用这个方法将render
方法返回的子节点给链表起:
function link(parent, elements) {
if (elements === null) elements = [];
parent.child = elements.reduceRight((previous, current) => {
const node = new Node(current);
node.return = parent;
node.sibling = previous;
return node;
}, null);
return parent.child;
}
复制代码
这个方法从最后一个元素开始迭代一组节点,而后将它们连接成一个单链表。它返回列表的第一个兄弟节点,这里有个关于它如何工做的简单案例:
const children = [{name: 'b1'}, {name: 'b2'}];
const parent = new Node({name: 'a1'});
const child = link(parent, children);
// the following two statements are true
console.log(child.instance.name === 'b1');
console.log(child.sibling.instance === children[1]);
复制代码
咱们也实现了一个帮助方法来执行节点的一些工做。在咱们的案例中,咱们将打印组件的名称,初次以后,还会收集组件的子节点,并将它们连接起来。
好,如今咱们准备实现主要的循环算法,它是父节点优先、深度优先实现。这里有它的附加注释的代码:
function walk(o) {
let root = o;
let current = o;
while (true) {
// perform work for a node, retrieve & link the children
let child = doWork(current);
// if there's a child, set it as the current active node
if (child) {
current = child;
continue;
}
// if we've returned to the top, exit the function
if (current === root) {
return;
}
// keep going up until we find the sibling
while (!current.sibling) {
// if we've returned to the top, exit the function
if (!current.return || current.return === root) {
return;
}
// set the parent as the current active node
current = current.return;
}
// if found, set the sibling as the current active node
current = current.sibling;
}
}
复制代码
虽然实现不是特别的难理解,但你可能须要稍微执行来领会它。思路是,咱们保持当前节点的引用,在沿着树往下时,重复给其赋值,直到到达树枝的末尾,而后,再使用return
指针返回给共同的父节点。
若是咱们如今检查这个实现的调用栈时,能够看到:
正如你看到的,这个栈不会随着往下遍历树时增加,可是若是你在doWork
方法中加上dubugger,且打印节点的名称,咱们就会看到以下状况:
**这看起来想是一个浏览器的调用栈。**因此以这个算法,咱们用本身的实现有效地替换了浏览的调用栈实现。这正如Andrew描述的:
Fiber 是栈的从新实现,特别针对于React组件,你能够认为一个fiber就是一个虚拟的栈帧。
至此,咱们如今经过保持做为顶层帧的节点的引用来控制着栈:
function walk(o) {
let root = o;
let current = o;
while (true) {
...
current = child;
...
current = current.return;
...
current = current.sibling;
}
}
复制代码
咱们能够在任意时刻中止遍历,以后再继续它。这确实咱们可以用在新requestIdleCallback
API而想要实现的状况。
这里的代码实现了React中工做循环:
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
复制代码
正如你看到的,它很好地对应了咱们上面所说的算法。它在做为顶层帧的nextUnitOfWork
变量中保持了当前fiber节点的引用。
这个算法能够同步遍历组件树,且执行树中每一个fiber节点的工做(nextUnitOfWork)。这个一般是由UI事件形成的所谓互动更新(click, input, etc)。或者它能够在执行一个fiber节点的工做后,检测是否还有剩余时间,来异步遍历组件树。方法shouldYield
返回基于 deadlineDidExpire 和 deadline 变量的结果,这些变量会在React执行fiber节点工做时不断地更新。
**peformUnitOfWork**
方法深度解析在这。