大家好,我是「柒八九」。
前段时间,我们开辟了,「前端框架」的文章系列,首先就介绍了,关于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(<App />, document.getElementById('root'))
语法开始探索。
ReactDOM
模块将<App/ >
传递给调和器,但这里有两个问题:
<App />
指的是什么?- 什么是调和器?
让我们来一一解答这些问题。
<App />
指的是什么?
<App />
是一个React元素
。根据 React博客描述,”元素是一个描述组件实例或DOM节点及其所需属性的「普通对象」“。
换句话说,元素「不是实际的DOM节点或组件实例」;它们是一种向 React
描述它们是什么类型的元素,它们拥有什么属性,以及它们的孩子是谁的信息组织方式。
❝
React 元素
在早期的React介绍文档中,有另外一个家喻户晓的名字:「虚拟DOMVirtual-DOM」 只不过,V-Dom
在理解上在某些场景下会产生歧义,所以逐渐被React 元素
所替代 ❞
这就是 React
的真正力量所在。React
将如何构建、渲染和管理实际DOM树的生命周期的复杂部分「抽象出来」,有效地使开发者的开发变得更容易。
为了理解React 元素
所带来的好处,让我们看一下使用面向对象Object-Oriented的传统方法解决一个页面逻辑的开发,到底经历些什么。
React中的OOP(面向对象编程)
在传统的面向对象编程中,开发者必须实例化并管理每个DOM元素的生命周期。例如,如果你想创建一个简单的表单和一个提交按钮,它们的状态信息仍然需要开发者来维护。
让我们假设 Button
组件有一个 isSubmitted
「状态变量」。Button
组件的生命周期看起来像下面的流程图,其中「每个状态都必须由开发者管理」。
流程图的大小和代码行数随着状态变量数量的增加而呈「指数级增长」。
所以,React
使用元素来解决这个问题;在 React
有两种元素:「DOM元素」和「组件元素」。
- 「DOM元素是一个字符串的元素」
例如,
<button class="okButton"> OK </button>
- 「组件元素是一个类或一个函数」
例如,
<Button className="okButton"> OK </Button>
,其中<Button>
是「一个类或一个函数组件」。
❝这两种类型都是「简单的对象」。 它们仅仅是对在屏幕上「渲染的内容的描述」,在你创建和实例化它们的时候,「并不会发生渲染操作」。 ❞
React 调和算法Reconciliation
该算法使得 React
更容易解析和遍历应用,用以建立对应的DOM树。「实际的渲染工作会在遍历完成后发生」。
当 React
遇到一个类或一个函数组件时,它会基于元素的props
来渲染UI视图。
例如,如果<App>
组件渲染了以下内容,那么 React
会遍历<Form>
和<Button>
组件,它们想根据相应的 props
渲染成什么。
<Form>
<Button>
Submit
</Button>
</Form>
Form
组件是函数组件,React
将调用render()
来了解它所要渲染的元素,得知它要渲染一个有孩子节点的<div>
。
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. 递归操作
在上文介绍「堆栈调和器」中得知,在进行调和处理时,会执行「递归操作」,而递归操作和「调用栈」有很大的关系,进而我们可以得出,递归和「堆栈」也有千丝万缕的联系。
用一个简单的例子,看看在「调用栈」中会发生什么。
代码语言:javascript复制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
)并执行了一个「父级优先」、「深度优先」的遍历。
后记
「分享是一种态度」。
参考资料:
- how-react-fiber-work
- React官网