前端小魔女

V1

2022/06/21阅读:18主题:蔷薇紫

React_Fiber机制(下)

不念过去,不畏未来
在群星闪耀的过去与无限可能的未来之间,是现在

大家好,我是柒八九

前段时间,我们开辟了,前端框架的文章系列,首先就介绍了,关于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介绍文档中,有另外一个家喻户晓的名字: 虚拟DOM Virtual-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-domreact-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节点有效地持有组件的stateprops和它所渲染的DOM元素。

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

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

后记

分享是一种态度

参考资料:

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

分类:

前端

标签:

React.js

作者介绍

前端小魔女
V1