React_Fiber机制(下)

2022-08-25 15:14:27 浏览数 (1)

大家好,我是「柒八九」

前段时间,我们开辟了,「前端框架」的文章系列,首先就介绍了,关于React-Fiber的相关机制。由于文章行文结构所制约下,针对一些边界情况,没有展开介绍。

而今天的这篇文章,就是为了查漏补缺的。有些比较重要的点,可能会再次提出。

好了,「话不多说,开搞」

你能所学到的知识点

  1. React-Fiber是个啥
  2. React旧有的堆栈调和器Stack Reconciler存在什么问题
  3. 页面丢帧的原因
  4. React-Fiber的工作原理

文章概要

  1. React-Fiber是个啥
  2. 堆栈调和器Stack Reconciler
  3. 递归操作
  4. 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 渲染成什么。

代码语言:javascript复制
<Form>
  <Button>
    Submit
  </Button>
</Form>

Form 组件是函数组件,React 将调用render()来了解它所要渲染的元素,得知它要渲染一个有孩子节点的<div>

代码语言:javascript复制
const Form = (props) => {
  return(
    <div className="form">
      {props.form}
    </div>
  )
}

React「重复这个过程」,直到它掌握了页面上与每个组件所对应的DOM元素的相关渲染信息。

❝这种通过「递归元素树」,以掌握React应用的组件树的DOM元素的过程,被称为「调和」。 ❞

在调和结束时,React 知道DOM树的结果,像 react-domreact-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 引擎首先创建一个全局执行上下文,并将其推入执行栈。

代码语言: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节点有效地持有组件的stateprops和它所渲染的DOM元素。

而且,由于fiber节点可变的,React 「不需要为更新而重新创建每个节点;它可以简单地克隆并在有更新时更新节点」

fiber树的情况下,React 并不执行递归遍历。相反,它创建了一个「单链的列表」,(Effect-List)并执行了一个「父级优先」「深度优先」的遍历。

后记

「分享是一种态度」

参考资料:

  • how-react-fiber-work
  • React官网

0 人点赞