当前位置:   article > 正文

React基础学习-Day07

React基础学习-Day07

React基础学习-Day07

Fiber

Fiber是什么?

首先,我们先聊聊React的基本组成:当我们写React组件并使用JSX时,React在底层会将JSX转换为元素的对象结构。例如:

const element = <h1>Hello, world</h1>;
  • 1

上述代码会被转换为以下形式:

const element = React.createElement(
  'h1',
  null,
  'Hello, world'
);
  • 1
  • 2
  • 3
  • 4
  • 5

为了将这个元素渲染到DOM上,React需要创建一种内部实例,用来追踪该组件的所有信息和状态。在早期版本的React中,我们称之为“实例”或“虚拟DOM对象”。但在Fiber架构中,这个新的工作单元就叫做Fiber。

所以,在本质上,Fiber是一个JavaScript对象,代表React的一个工作单元,它包含了与组件相关的信息。一个简化的Fiber对象长这样:

{
  type: 'h1',  // 组件类型
  key: null,   // React key
  props: { ... }, // 输入的props
  state: { ... }, // 组件的state (如果是class组件或带有state的function组件)
  child: Fiber | null,  // 第一个子元素的Fiber
  sibling: Fiber | null,  // 下一个兄弟元素的Fiber
  return: Fiber | null,  // 父元素的Fiber
  // ...其他属性
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当React开始工作时,它会沿着Fiber树形结构进行,试图完成每个Fiber的工作(例如,比较新旧props,确定是否需要更新组件等)。如果主线程有更重要的工作(例如,响应用户输入),则React可以中断当前工作并返回执行主线程上的任务。

因此,Fiber不仅仅是代表组件的一个内部对象,它还是React的调度和更新机制的核心组成部分。

为什么需要Fiber?

在React 16之前的版本中,是使用递归的方式处理组件树更新,称为堆栈调和(Stack Reconciliation),这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。

Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。

Fiber的结构

我们来看一下源码里FiberNode的结构:

function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 基本属性
  this.tag = tag; // 描述此Fiber的启动模式的值(LegacyRoot = 0; ConcurrentRoot = 1)
  this.key = key; // React key
  this.elementType = null; // 描述React元素的类型。例如,对于JSX<App />,elementType是App
  this.type = null; // 组件类型
  this.stateNode = null; // 对于类组件,这是类的实例;对于DOM元素,它是对应的DOM节点。

  // Fiber链接
  this.return = null; // 指向父Fiber
  this.child = null; // 指向第一个子Fiber
  this.sibling = null; // 指向其兄弟Fiber
  this.index = 0; // 子Fiber中的索引位置

  this.ref = null; // 如果组件上有ref属性,则该属性指向它
  this.refCleanup = null; // 如果组件上的ref属性在更新中被删除或更改,此字段会用于追踪需要清理的旧ref

    // Props & State
  this.pendingProps = pendingProps; // 正在等待处理的新props
  this.memoizedProps = null; // 上一次渲染时的props
  this.updateQueue = null; // 一个队列,包含了该Fiber上的状态更新和副作用
  this.memoizedState = null; // 上一次渲染时的state
  this.dependencies = null; // 该Fiber订阅的上下文或其他资源的描述

    // 工作模式
  this.mode = mode; // 描述Fiber工作模式的标志(例如Concurrent模式、Blocking模式等)。

  // Effects
  this.flags = NoFlags; // 描述该Fiber发生的副作用的标志(十六进制的标识)
  this.subtreeFlags = NoFlags; // 描述该Fiber子树中发生的副作用的标志(十六进制的标识)
  this.deletions = null; // 在commit阶段要删除的子Fiber数组

  this.lanes = NoLanes; // 与React的并发模式有关的调度概念。
  this.childLanes = NoLanes; // 与React的并发模式有关的调度概念。

  this.alternate = null; // Current Tree和Work-in-progress (WIP) Tree的互相指向对方tree里的对应单元

    // 如果启用了性能分析
  if (enableProfilerTimer) {
    // ……
  }

    // 开发模式中
  if (__DEV__) {
    // ……
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

其实可以理解为是一个更强大的虚拟DOM。

Fiber工作原理

Fiber工作原理中最核心的点就是:可以中断和恢复,这个特性增强了React的并发性和响应性。

实现可中断和恢复的原因就在于:Fiber的数据结构里提供的信息让React可以追踪工作进度、管理调度和同步更新到DOM

现在我们来聊聊Fiber工作原理中的几个关键点:

  • 单元工作:每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。

  • 链接属性childsiblingreturn 字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。

    React Fiber https://weijunext.com

  • 双缓冲技术: React在更新时,会根据现有的Fiber树(Current Tree)创建一个新的临时树(Work-in-progress (WIP) Tree),WIP-Tree包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree是当前显示在页面上的视图,WIP-Tree则是在后台进行更新,WIP-Tree更新完成后会复制其它节点,并最终替换掉Current Tree,成为新的Current Tree。因为React在更新时总是维护了两个Fiber树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让React能够同时具备拥有优秀的渲染性能和UI的稳定性。

React Fiber https://weijunext.com

  • State 和 Props:memoizedPropspendingPropsmemoizedState 字段让React知道组件的上一个状态和即将应用的状态。通过比较这些值,React可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。
  • 副作用的追踪flagssubtreeFlags 字段标识Fiber及其子树中需要执行的副作用,例如DOM更新、生命周期方法调用等。React会积累这些副作用,然后在Commit阶段一次性执行,从而提高效率。

资料转载:

React Fiber很难?六个问题助你理解 React Fiber

全文大量参考和引用以下几篇博文,读者可自行查阅:

一、React 的设计理念是什么?

React官网在React哲学一节开篇提到:

我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。React 最棒的部分之一是引导我们思考如何构建一个应用。

由此可见,React 追求的是 “快速响应”,那么,“快速响应“的制约因素都有什么呢?

  • CPU的瓶颈:当项目变得庞大、组件数量繁多、遇到大计算量的操作或者设备性能不足使得页面掉帧,导致卡顿。
  • IO的瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

本文要聊的fiber 架构主要就是用来解决 CPU 和网络的问题,这两个问题一直也是最影响前端开发体验的地方,一个会造成卡顿,一个会造成白屏。为此 react 为前端引入了两个新概念:Time Slicing 时间分片和Suspense

二、React的“先天不足” —— 听说 Vue 3.0 采用了动静结合的 Dom diff,React 为何不跟进?

Vue 3.0 动静结合的 Dom diff

Vue3.0 提出动静结合的 DOM diff 思想,动静结合的 DOM diff其实是在预编译阶段进行了优化。之所以能够做到预编译优化,是因为 Vue core 可以静态分析 template,在解析模版时,整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签和文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

借助预编译过程,Vue 可以做到的预编译优化就很强大了。比如在预编译时标记出模版中可能变化的组件节点,再次进行渲染前 diff 时就可以跳过“永远不会变化的节点”,而只需要对比“可能会变化的动态节点”。这也就是动静结合的 DOM diff 将 diff 成本与模版大小正相关优化到与动态节点正相关的理论依据

React 能否像 Vue 那样进行预编译优化?

Vue 需要做数据双向绑定,需要进行数据拦截或代理,那它就需要在预编译阶段静态分析模版,分析出视图依赖了哪些数据,进行响应式处理。而 React 就是局部重新渲染,React 拿到的或者说掌管的,所负责的就是一堆递归 React.createElement 的执行调用(参考下方经过Babel转换的代码),它无法从模版层面进行静态分析。JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足

JSX 写法:

<div>
  <h1>六个问题助你理解 React Fiber</h1>
  <ul>
    <li>React</li>
    <li>Vue</li>
  </ul>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

递归 React.createElement:

// Babel转换后
React.createElement(
  "div",
  null,
 React.createElement(
    "h1",
    null,
    "\u516D\u4E2A\u95EE\u9898\u52A9\u4F60\u7406\u89E3 React Fiber"
  ),
 React.createElement(
    "ul",
    null,
   React.createElement("li", null, "React"),
   React.createElement("li", null, "Vue")
  )
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

JSX vs Template

  • JSX 具有 JavaScript 的完整表现力,可以构建非常复杂的组件。但是灵活的语法,也意味着引擎难以理解,无法预判开发者的用户意图,从而难以优化性能。
  • Template 模板是一种非常有约束的语言,你只能以某种方式去编写模板。

既然存在以上编译时先天不足,在运行时优化方面,React一直在努力。比如,React15实现了batchedUpdates(批量更新)。即同一事件回调函数上下文中的多次setState只会触发一次更新。

但是,如果单次更新就很耗时,页面还是会卡顿(这在一个维护时间很长的大应用中是很常见的)。这是因为React15的更新流程是同步执行的,一旦开始更新直到页面渲染前都不能中断。

资料参考:以 React 为例,说说框架和性能(下) | 新兴前端框架 Svelte 从入门到原理

三、从架构演变看不断进击的 React 都做过哪些优化?

React渲染页面的两个阶段

  • 调度阶段(reconciliation):在这个阶段 React 会更新数据生成新的 Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列
  • 渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到DOM上

React 15 架构

React15架构可以分为两层:

  • Reconciler(协调器)—— 负责找出变化的组件;
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上;

在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。

为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。

React 16 架构

为了解决同步更新长时间占用线程导致页面卡顿的问题,也为了探索运行时优化的更多可能,React开始重构并一直持续至今。重构的目标是实现Concurrent Mode(并发模式)。

从v15到v16,React团队花了两年时间将源码架构中的Stack Reconciler重构为Fiber Reconciler。

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler;
  • Reconciler(协调器)—— 负责找出变化的组件:更新工作从递归变成了可以中断的循环过程。Reconciler内部采用了Fiber的架构
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上。

React 17 优化

React16的expirationTimes模型只能区分是否>=expirationTimes决定节点是否更新。React17的lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。

Lane用二进制位表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是Lane所要解决的问题。

Concurrent Mode的目的是实现一套可中断/恢复的更新机制。其由两部分组成:

  • 一套协程架构:Fiber Reconciler
  • 基于协程架构的启发式更新算法:控制协程架构工作方式的算法

资料参考:React17新特性:启发式更新算法

四、浏览器一帧都会干些什么以及requestIdleCallback的启示

浏览器一帧都会干些什么?

我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?

浏览器一帧都会干些什么

通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 绘制渲染
  7. 执行 RIC (RequestIdelCallback)

第七步的 RIC 事件不是一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。

requestIdleCallback 的启示

我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。

requestIdleCallback((deadline) => {
// deadline 有两个参数
  // timeRemaining(): 当前帧还剩下多少时间
  // didTimeout: 是否超时
// 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 则会强制浏览器在当前帧执行完后执行。
 if (deadline.timeRemaining() > 0) {
   // TODO
 } else {
  requestIdleCallback(otherTasks);
 }
});

// 用法示例
var tasksNum = 10000

requestIdleCallback(unImportWork)

function unImportWork(deadline) {
  while (deadline.timeRemaining() && tasksNum > 0) {
    console.log(`执行了${10000 - tasksNum + 1}个任务`)
    tasksNum--
  }
  if (tasksNum > 0) { // 在未来的帧中继续执行
    requestIdleCallback(unImportWork)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

其实部分浏览器已经实现了这个API,这就是requestIdleCallback。但是由于以下因素,Facebook 抛弃了 requestIdleCallback 的原生 API:

  • 浏览器兼容性;
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低。

参考: requestIdleCallback 的 FPS 只有 20

基于以上原因,在React中实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

资料参考:requestIdleCallback-后台任务调度

五、 Fiber 为什么是 React 性能的一个飞跃?

什么是 Fiber

Fiber 的英文含义是“纤维”,它是比线程(Thread)更细的线,比线程(Thread)控制得更精密的执行模型。在广义计算机科学概念中,Fiber 又是一种协作的(Cooperative)编程模型(协程),帮助开发者用一种【既模块化又协作化】的方式来编排代码。

在 React 中,Fiber 就是 React 16 实现的一套新的更新机制,让 React 的更新过程变得可控,避免了之前采用递归需要一气呵成影响性能的做法

React Fiber 中的时间分片

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

Stack Reconciler

基于栈的 Reconciler,浏览器引擎会从执行栈的顶端开始执行,执行完毕就弹出当前执行上下文,开始执行下一个函数,直到执行栈被清空才会停止。然后将执行权交还给浏览器。由于 React 将页面视图视作一个个函数执行的结果。每一个页面往往由多个视图组成,这就意味着多个函数的调用。

如果一个页面足够复杂,形成的函数调用栈就会很深。每一次更新,执行栈需要一次性执行完成,中途不能干其他的事儿,只能"一心一意"。结合前面提到的浏览器刷新率,JS 一直执行,浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿。不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟。

Fiber Reconciler

链表结构

在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。在 React 16 中使用了大量的链表。

  • 使用多向链表的形式替代了原来的树结构;
<div id="A">
  A1
  <div id="B1">
    B1
    <div id="C1"></div>
  </div>
  <div id="B2">
    B2
  </div>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

多向链表

  • 副作用单链表;

副作用单链表

  • 状态更新单链表;

状态更新单链表

链表是一种简单高效的数据结构,它在当前节点中保存着指向下一个节点的指针;遍历的时候,通过操作指针找到下一个元素。

链表

链表相比顺序结构数据格式的好处就是:

  1. 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了。
  2. 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点。

但链表也不是完美的,缺点就是:

  1. 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针。
  2. 不能自由读取,必须找到他的上一个节点。

React 用空间换时间,更高效的操作可以方便根据优先级进行操作。同时可以根据当前节点找到其他节点,在下面提到的挂起和恢复过程中起到了关键作用

斐波那契数列的 Fiber

递归形式的斐波那契数列写法:

function fib(n) {
  if (n <= 2) {
    return 1;
  } else {
    return fib(n - 1) + fib(n - 2);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

采用 Fiber 的思路将其改写为循环(这个例子并不能和 React Fiber 的对等):

function fib(n) {
  let fiber = { arg: n, returnAddr: null, a: 0 }, consoled = false;
  // 标记循环
  rec: while (true) {
    // 当展开完全后,开始计算
    if (fiber.arg <= 2) {
      let sum = 1;
      // 寻找父级
      while (fiber.returnAddr) {
        if(!consoled) {
          // 在这里打印查看形成的链表形式的 fiber 对象
          consoled=true
          console.log(fiber)
        }
        fiber = fiber.returnAddr;
        if (fiber.a === 0) {
          fiber.a = sum;
          fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
          continue rec;
        }
        sum += fiber.a;
      }
      return sum;
    } else {
      // 先展开
      fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 };
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

六、React Fiber 是如何实现更新过程可控?

更新过程的可控主要体现在下面几个方面:

  • 任务拆分
  • 任务挂起、恢复、终止
  • 任务具备优先级

任务拆分

在 React Fiber 机制中,它采用"化整为零"的思想,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。

任务挂起、恢复、终止

workInProgress tree

workInProgress 代表当前正在执行更新的 Fiber 树。在 render 或者 setState 后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链

currentFiber tree

currentFiber 表示上次渲染构建的 Filber 树在每一次更新完成后 workInProgress 会赋值给 currentFiber。在新一轮更新时 workInProgress tree 再重新构建,新 workInProgress 的节点通过 alternate 属性和 currentFiber 的节点建立联系。

在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。

挂起

当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。

  1. 如何判断一帧是否有空闲时间的呢?

使用前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。

  1. 恢复执行的时候又是如何知道下一个任务是什么呢?

答案是在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。

终止

其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

workInProgress tree 构建

任务具备优先级

React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行该任务。过期时间的大小还代表着任务的优先级

任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表 A1(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。

其实最终都是为了收集到这条副作用链表,有了它,在接下来的渲染阶段就通过遍历副作用链完成 DOM 更新。这里需要注意,更新真实 DOM 的这个动作是一气呵成的,不能中断,不然会造成视觉上的不连贯(commit)。

<div id="A1">
  A1
  <div id="B1">
    B1
    <div id="C1">C1</div>
    <div id="C2">C2</div>
  </div>
  <div id="B2">
    B2
  </div>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

副作用链

Fiber 结构长什么样?

基于时间分片的增量更新需要更多的上下文信息,之前的vDOM tree显然难以满足,所以扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress tree)。

FiberNode 上的属性有很多,根据笔者的理解,以下这么几个属性是值得关注的:return、child、sibling(主要负责fiber链表的链接);stateNode;effectTag;expirationTime;alternate;nextEffect。各属性介绍参看下面的class FiberNode

class FiberNode {
  constructor(tag, pendingProps, key, mode) {
    // 实例属性
    this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...
    this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是最终 ReactElement 上的
    this.elementType = null; // createElement的第一个参数,ReactElement 上的 type
    this.type = null; // 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样
    this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象
    // fiber
    this.return = null; // 父节点,指向上一个 fiber
    this.child = null; // 子节点,指向自身下面的第一个 fiber
    this.sibling = null; // 兄弟组件, 指向一个兄弟节点
    this.index = 0; //  一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
    this.ref = null; // reactElement 上的 ref 属性
    this.pendingProps = pendingProps; // 新的 props
    this.memoizedProps = null; // 旧的 props
    this.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新
    this.memoizedState = null; // 对应  memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
    this.mode = mode; // 表示当前组件下的子组件的渲染方式
    // effects
    this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新(更新、删除等)
    this.nextEffect = null; // 指向下个需要更新的fiber
    this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个
    this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个
    this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成
    this.childExpirationTime = NoWork; // child 过期时间
    this.alternate = null; // current 树和 workInprogress 树之间的相互引用
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

fiber-tree

图片来源:完全理解React Fiber

js

 代码解读
复制代码function performUnitWork(currentFiber){
    //beginWork(currentFiber) //找到儿子,并通过链表的方式挂到currentFiber上,没有儿子就找后面那个兄弟
  //有儿子就返回儿子
  if(currentFiber.child){
    return currentFiber.child;
  } 
  //如果没有儿子,则找弟弟
  while(currentFiber){//一直往上找
    //completeUnitWork(currentFiber);//将自己的副作用挂到父节点去
    if(currentFiber.sibling){
      return currentFiber.sibling
    }
    currentFiber = currentFiber.return;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

Concurrent Mode (并发模式)

Concurrent Mode 指的就是 React 利用上面 Fiber 带来的新特性的开启的新模式 (mode)。react17开始支持concurrent mode,这种模式的根本目的是为了让应用保持cpu和io的快速响应,它是一组新功能,包括Fiber、Scheduler、Lane,可以根据用户硬件性能和网络状况调整应用的响应速度,核心就是为了实现异步可中断的更新。concurrent mode也是未来react主要迭代的方向。

目前 React 实验版本允许用户选择三种 mode:

  1. Legacy Mode: 就相当于目前稳定版的模式
  2. Blocking Mode: 应该是以后会代替 Legacy Mode 而长期存在的模式
  3. Concurrent Mode: 以后会变成 default 的模式

Concurrent Mode 其实开启了一堆新特性,其中有两个最重要的特性可以用来解决我们开头提到的两个问题:

  1. Suspense:Suspense 是 React 提供的一种异步处理的机制, 它不是一个具体的数据请求库。它是React 提供的原生的组件异步调用原语。
  2. useTrasition:让页面实现 Pending -> Skeleton -> Complete 的更新路径, 用户在切换页面时可以停留在当前页面,让页面保持响应。 相比展示一个无用的空白页面或者加载状态,这种用户体验更加友好。

其中 Suspense 可以用来解决请求阻塞的问题,UI 卡顿的问题其实开启 concurrent mode 就已经解决的,但如何利用 concurrent mode 来实现更友好的交互还是需要对代码做一番改动的。

资料参考:Concurrent 模式介绍 (实验性) | 理解 React Fiber & Concurrent Mode | 11.concurrent mode(并发模式是什么样的) | 人人都能读懂的react源码解析

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/857402
推荐阅读
相关标签
  

闽ICP备14008679号