内容简介:React中,改变检测通常被看作是协调(reconciliation)或者渲染(rendering),而Fiber正是这个机制的一种新的实现。在这个架构之下,可以实现一些有趣特性,如:改善非阻塞渲染,执行基于优先级的更新,以及在后台提前渲染内容。这些特性在如果你现在Google查询“ReactFiber”,你会搜到大量的相关文章,这些除了我们开始吧。
React中,改变检测通常被看作是协调(reconciliation)或者渲染(rendering),而Fiber正是这个机制的一种新的实现。在这个架构之下,可以实现一些有趣特性,如:改善非阻塞渲染,执行基于优先级的更新,以及在后台提前渲染内容。这些特性在 并发React哲学 中被认为是 time-slicing 。除了解决开发者一些真实问题外, 这些机制的内部实现,从工程角度来看也具有广泛的吸引力。关于其中缘由的有价值的知识点将有助于我们作为开发者得到成长 。
如果你现在Google查询“ReactFiber”,你会搜到大量的相关文章,这些除了 Andrew Clark的笔记 都是相当高层面的讲解。本篇文章中我将引用这个资源, 并且提供一个细致的关于Fiber中一些特别重要概念的讲解 。当我们结束时,你将对 Lin Clark在ReactConf 2017上关于工作方法(work loop)的演讲 有足够的知识来理解,这个演讲你需要看一下,不过在你大致看完之后,我将让你有更多的理解。 一篇解析一系列关于React Fiber内幕的文章 ,我大概花了70%的时间用来理解其内部的详细实现,过程中还写了三篇关于协调和渲染机制的文章。
我们开始吧。
设立一个背景
Fiber的架构有两个主要阶段:协调/渲染(reconciliation/render)和提交(commit)。在源码中协调阶段基本上被当作是“渲染阶段(render phase)”。这个阶段中,React会遍历组件树并且会:
- 更新state和props
- 执行生命周期钩子函数
- 获取子组件
- 新旧子组件比较
- 整理出需要执行的DOM更新
所有这些操作在Fiber中被认为是一个work。需要操作的work的类型取决于React Element的类型,例如,对于一个 Class Component
React需要实例化一个类,但对于 Function Component
则不需要。如果感兴趣,你可以在 这里 看到Fiber中所有work对象的类型。这些操作确实如Andrew演讲中所提到的:
当处理一些UI时,有一个问题是,如果一次性执行太多的操作,那么将会导致动画掉帧
那“一次性”指的是什么呢?一般来说,React会 同步 遍历整个组件树,并且执行每个组件的work,而执行它逻辑的时间可能超出了16ms。这便导致之了掉帧,继而引起视图卡顿。
那,这有什么办法可以解决吗?
现代浏览器(包括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相关呢?
正如这篇文章第一部分中所说,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关于该算法的大致说明。为了实现这个算法,我们需要一个数据结构,包含三个字段:
- child — 代表第一个子节点
- sibling — 代表第一个兄弟节点
- return — 代表父节点
在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中的工作循环
这里的代码 实现了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**
方法深度解析在这。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- React Fiber 渐进式组件遍历详解
- React 折腾记 - (11) 结合Antd菜单控件(递归遍历组件)及常规优化
- 「译」如何以及为什么 React Fiber 使用链表遍历组件树
- 数组常见的遍历循环方法、数组的循环遍历的效率对比
- C++拾趣——STL容器的插入、删除、遍历和查找操作性能对比(Windows VirtualStudio)——遍历和删除
- Js遍历数组总结
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
ACM国际大学生程序设计竞赛
俞勇 编 / 2012-12 / 29.00元
《ACM国际大学生程序设计竞赛:知识与入门》适用于参加ACM国际大学生程序设计竞赛的本科生和研究生,对参加青少年信息学奥林匹克竞赛的中学生也很有指导价值。同时,作为程序设计、数据结构、算法等相关课程的拓展与提升,《ACM国际大学生程序设计竞赛:知识与入门》也是难得的教学辅助读物。一起来看看 《ACM国际大学生程序设计竞赛》 这本书的介绍吧!