不念过去,不畏未来。
在群星闪耀的过去与无限可能的未来之间,是现在
大家好,我是柒八九。
前段时间,我们开辟了,前端框架的文章系列,首先就介绍了,关于React-Fiber的相关机制。由于文章行文结构所制约下,针对一些边界情况,没有展开介绍。
而今天的这篇文章,就是为了查漏补缺的。有些比较重要的点,可能会再次提出。
好了,话不多说,开搞。
你能所学到的知识点
React-Fiber
是个啥React
旧有的{堆栈调和器| Stack Reconciler}存在什么问题- 页面丢帧的原因
React-Fiber
的工作原理
文章概要
- React-Fiber是个啥
- {堆栈调和器| Stack Reconciler}
- 递归操作
- React Fiber 如何工作的
1. React-Fiber是个啥
React Fiber
是一个内部引擎,旨在使React
更快、更智能。
{Fiber 调和器| Fiber Reconciler}成为 React 16+
版本的默认调和器,它完全重写了 React
原有的调和算法,以解决 React
中一些长期存在的问题。
因为 Fiber
是{异步| Asynchronous}的,React可以:
- 当新的更新发生时,暂停、恢复和重新启动组件的渲染工作
- 重复使用以前完成的工作,如果不再需要,甚至可以丢弃它
- 将工作分成几块,并根据重要性来确定任务的优先次序
在调和过程中有很多操作, 例如调用生命周期方法或者更新
ref
等。所有这些操作在Fiber
架构中都被统称为 {工作| Work}。
工作的类型通常取决于
React
元素的类型
这一变化使 React
摆脱了{同步堆栈调节器| Synchronous Stack Reconciler}的限制。以前,你可以添加或删除组件,但必须等调用堆栈为空,而且任务不能被中断。
使用新的调节器,也确保最重要的更新尽快发生。(更新存在优先级)
在了解Fiber 调和器
之前,我们先来简单了解下原来的调节算法:堆栈调和器。
2. {堆栈调和器| Stack Reconciler}
为什么这被称为 "堆栈 "调节器?这个名字来自于 "堆栈 "数据结构,它是一个后进先出的机制。
我们从最熟悉的ReactDOM.render(, document.getElementById('root'))
语法开始探索。
ReactDOM
模块将传递给调和器,但这里有两个问题:
指的是什么?
- 什么是调和器?
让我们来一一解答这些问题。
指的是什么?
是一个
React元素
。根据 React博客描述,”元素是一个描述组件实例或DOM节点及其所需属性的普通对象“。
换句话说,元素不是实际的DOM节点或组件实例;它们是一种向 React
描述它们是什么类型的元素,它们拥有什么属性,以及它们的孩子是谁的信息组织方式。
React 元素
在早期的React介绍文档中,有另外一个家喻户晓的名字: {虚拟DOM| Virtual-DOM}只不过,
V-Dom
在理解上在某些场景下会产生歧义,所以逐渐被React 元素
所替代
这就是 React
的真正力量所在。React
将如何构建、渲染和管理实际DOM树的生命周期的复杂部分抽象出来,有效地使开发者的开发变得更容易。
为了理解React 元素
所带来的好处,让我们看一下使用{面向对象| Object-Oriented}的传统方法解决一个页面逻辑的开发,到底经历些什么。
React中的OOP(面向对象编程)
在传统的面向对象编程中,开发者必须实例化并管理每个DOM元素的生命周期。例如,如果你想创建一个简单的表单和一个提交按钮,它们的状态信息仍然需要开发者来维护。
让我们假设 Button
组件有一个 isSubmitted
状态变量。Button
组件的生命周期看起来像下面的流程图,其中每个状态都必须由开发者管理。
流程图的大小和代码行数随着状态变量数量的增加而呈指数级增长。
所以,React
使用元素来解决这个问题;在 React
有两种元素:DOM元素和组件元素。
- DOM元素是一个字符串的元素
例如,OK
- 组件元素是一个类或一个函数
例如,OK
,其中 是一个类或一个函数组件。
这两种类型都是简单的对象。
它们仅仅是对在屏幕上渲染的内容的描述,在你创建和实例化它们的时候,并不会发生渲染操作。
React {调和算法| Reconciliation}
该算法使得 React
更容易解析和遍历应用,用以建立对应的DOM树。实际的渲染工作会在遍历完成后发生。
当 React
遇到一个类或一个函数组件时,它会基于元素的props
来渲染UI视图。
例如,如果组件渲染了以下内容,那么
React
会遍历和组件,它们想根据相应的 props
渲染成什么。
<Form> <Button> Submit </Button> </Form> 复制代码
Form
组件是函数组件,React
将调用render()
来了解它所要渲染的元素,得知它要渲染一个有孩子节点的
。
const Form = (props) => { return( <div className="form"> {props.form} </div> ) } 复制代码
React
会重复这个过程,直到它掌握了页面上与每个组件所对应的DOM元素的相关渲染信息。
这种通过递归元素树,以掌握
React
应用的组件树的DOM元素的过程,被称为调和。
在调和结束时,React
知道DOM树的结果,像 react-dom
或 react-native
这些渲染器渲染更新DOM节点所需的最小变化集。这意味着,当你调用 ReactDOM.render()
或 setState()
时,React
就会执行调和处理。
在 setState
的情况下,它执行了一个遍历,并通过将新的树与渲染的树进行比较来确定树中的变化。然后,它将这些变化应用到当前树上。
3. 递归操作
在上文介绍堆栈调和器中得知,在进行调和处理时,会执行递归操作,而递归操作和调用栈有很大的关系,进而我们可以得出,递归和堆栈也有千丝万缕的联系。
用一个简单的例子,看看在调用栈中会发生什么。
function fib(n) { if (n < 2){ return n } return fib(n - 1) + fib (n - 2) } fib(3) 复制代码
我们可以看到,调用堆栈将对fib()
的每一次调用都推入堆栈,直到弹出fib(1)
(第一个返回的函数调用)。
我们刚才看到的调和算法是一个纯粹的递归算法。一个更新会导致整个子树立即重新渲染。虽然这很好用,但这也有一些局限性。
在用户界面中,没有必要让每个更新都立即显示;
事实上,这样做可能会造成浪费,导致帧数下降并降低用户体验。
另外,不同类型的更新有不同的优先级--动画更新必须比数据存储的更新完成得快。
页面{丢帧| dropped frames} 问题
{帧率| Frame Rate}
帧率是指连续图像出现在显示器上的频率。
我们在电脑屏幕上看到的一切都由屏幕上播放的图像或帧组成,其速度在眼睛看来是瞬间的。
可以把电脑显示屏想象成一本书,而书的页面是以某种速度播放的帧。相对而言,电脑显示屏只不过是一本自动翻页书,当屏幕上的事物发生变化时,它就会连续播放。
通常情况下,为了画面流畅和即时,视频的播放速度必须达到每秒30帧(FPS
)左右;任何更高的速度都能带来更好的体验。
现在大多数设备都是以60FPS
刷新屏幕,1/60=16.67ms
,这意味着每16ms就有一个新的帧显示。这个数字很重要,因为如果 React渲染器
在屏幕上渲染的时间超过16ms,浏览器就会丢弃该帧。
然而,在现实中,浏览器要做一些内部工作,所以你的所有工作必须在10ms内完成。当你不能满足这个预算时,帧率就会下降,内容就会在屏幕上抖动。这通常被称为 jank
,它对用户的体验有负面影响。
当然,对于静态和文本内容来说,这并不是一个大问题。但是在显示动画的情况下,这个数字就很关键了。
如果每次有更新时,React
调和算法都会遍历整个App树,并重新渲染,如果遍历的时间超过16ms,就会掉帧。
这也是许多人希望更新按优先级分类,而不是盲目地把每个更新都传给调和器。另外,许多人希望能够暂停并在下一帧恢复工作。这样一来,React可以更好地控制与16ms渲染预算的工作。
这导致React团队重写了调和算法,它被称为Fiber
。那么,让我们来看看Fiber是如何解决这个问题的。
4. React Fiber 如何工作的
总结一下实现Fiber
所需要的功能
- 为不同类型的工作分配优先权
- 暂停工作,以后再来处理
- 如果不再需要,就放弃工作
- 重复使用以前完成的工作
实现这样的事情的挑战之一是 JavaScript
引擎的工作方式和语言中缺乏线程。为了理解这一点,让我们简单地探讨一下 JavaScript
引擎如何处理执行上下文。
JavaScript的{执行堆栈| Execution Stack}
每当你在 JavaScript
中写一个函数,JavaScript
引擎就会创建一个函数执行上下文。
每次 JavaScript
引擎启动时,它都会创建一个全局执行上下文,以保存全局对象;例如,浏览器中的window
对象和Node.js
中的global
对象。JavaScript
使用一个堆栈数据结构来处理这两个上下文,也被称为执行堆栈。
因此,当存在如下代码时,JavaScript
引擎首先创建一个全局执行上下文,并将其推入执行栈。
function a() { console.log("i am a") b() } function b() { console.log("i am b") } a() 复制代码
然后,它为 a()
函数创建一个函数执行上下文。由于b()
是在a()
中调用的,它为b()
创建了另一个函数执行上下文,并将其推入堆栈。
当b()
函数返回时,引擎销毁了b()
的上下文。当我们退出a()
函数时,a()
的上下文被销毁。执行过程中的堆栈看起来像这样。
但是,当浏览器发出像HTTP
请求这样的异步事件时会发生什么?JavaScript
引擎是储存执行栈并处理异步事件,还是等待事件完成?
JavaScript
引擎在这里做了一些不同的事情:在执行堆栈的底部,JavaScript
引擎有一个队列数据结构,也被称为{事件队列| Event Queue}。事件队列处理异步调用。
JavaScript
引擎通过等待执行栈清空来处理队列中的项目。所以,每次执行栈清空时,JavaScript
引擎都会检查事件队列,从队列中弹出项目,并处理事件。
值得注意的是,只有当执行栈为空或者执行栈中唯一的项目是全局执行上下文时,
JavaScript
引擎才会检查事件队列。
虽然我们称它们为异步事件,但这里有一个微妙的区别:事件在到达队列时是异步的,但在实际处理时,它们并不是真正的异步。
回到我们的堆栈调节器,当 React
遍历树时,它在执行堆栈中这样做。所以,当更新发生时,它们会在事件队列中进行排队。只有当执行栈清空时,更新才被处理。
这正是Fiber解决的问题,它重新实现了具有智能功能的堆栈--例如,暂停、恢复和中止。
Fiber
是对堆栈的重新实现,专门用于React组件。可以把一个
Fiber
看成是一个虚拟的堆栈框架。
重新实现堆栈的好处是,你可以把堆栈帧保留在内存中,并随时随地执行它们。
简单地说,Fiber
代表了一个有自己的虚拟堆栈的工作单位。在以前的调和算法的实现中,React
创建了一棵对象树(React元素),这些对象是不可变的,并递归地遍历该树。
在当前的实现中,React
创建了一棵可变的Fiber节点树。Fiber
节点有效地持有组件的state
、props
和它所渲染的DOM元素。
而且,由于fiber
节点可变的,React
不需要为更新而重新创建每个节点;它可以简单地克隆并在有更新时更新节点。
在fiber
树的情况下,React
并不执行递归遍历。相反,它创建了一个单链的列表,(Effect-List
)并执行了一个父级优先、深度优先的遍历。
后记
分享是一种态度。
参考资料:
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。