react源码分析之Fiber

fiber

react 的 jsx 语法会被编译成 react.createElement:

React 用$$typeof 标识了一个 React element, 而每个这样的 element 对应了一个 fiber。

什么是 fiber

Fiber 是 React 16 引入的一种新的协调算法(Reconciliation Algorithm)和架构。它的核心是一个虚拟栈帧,可以理解为一种轻量级的 JavaScript 对象,用于描述组件树的结构和状态。

  • 一个 fiber 就是一个对象结构,包含了一系列要完成的任务。

  • react 的每一个 element 都对应了一个 fiber(一棵 elements 树就对应了一棵 fiber 节点树)。

  • 一个 fiber 不会在每次 render 中重新创建,相反,它是一个可以被操作改变的数据结构,保留了组件的状态和 dom。所以操作在每个 fiber 上任务(更新,删除等)都可以映射到对应的 element。

为什么要用 fiber

  • 暂停任务,并且可以稍后继续

  • 为不同的任务标记优先级

  • 重用之前完成的任务

  • 终止不再需要的任务

react 有了优先级调度(schedule)的能力, 也让 react 能把 reconciliation (计算哪一部分的 element 树需要被更新, 计算更新的这一步也被分为很多 unit, 防止阻塞主线程 ) 和 render( 使用那些计算好的更新信息, 把更新渲染到用户屏幕上) 分开, 使得 reconciliation 可以重用在不同平台上( react native,react dom)

几个重要的属性

{
    type: React.createElement 对应的type,表明这个fiber 节点对应的element
    tag: 表明fiber 的类型
    pendingProps: 已经是被更新的props,需要被运用到子组件或者dom 元素上
    key: 对应prop 上的key
    stateNode:  dom节点(HostComponent) / 类组件的实例 (ClassComponent) / fn() (FunctionComponent)
    nextEffect:  指向下一个**effect list**中的节点 (effect list:一个workInProgress(finishedWork)的子树,是在render阶段 最终需要决定被执行更新 的产物,会在commit阶段被处理)
    effectTag:  当前fiber需要执行的副作用类型
    alternate:  用于构成**workInProgress**(从当前fiber树构建而来,反应了需要被更新渲染到用户屏幕的状态树)
    return: 指向父fiber节点
    sibling: 指向兄弟fiber节点
    child: 指向child fiber节点
  }

fiber 与 fiber tree

react 运行时存在 3 中实例:

DOM 真实DOM节点
-------
Instances React维护的vDOM tree node
-------
Elements 描述UI长什么样子(type, props)

Instances 是根据 Elements 创建的,对组件及 DOM 节点的抽象表示,vDOM tree 维护了组件状态以及组件与 DOM 树的关系

在首次渲染过程中构建出 vDOM tree,后续需要更新时(setState()),diff vDOM tree 得到 DOM change,并把 DOM change 应用(patch)到 DOM 树

fiber 之前的 reconciler(被称为 Stack reconciler)自顶向下的递归 mount/update,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验

Fiber 解决这个问题的思路是把渲染/更新过程(递归 diff)拆分成一系列小任务,每次检查树上的一小部分,做完看是否还有时间继续下一个任务,有的话继续,没有的话把自己挂起,主线程不忙的时候再继续

增量更新需要更多的上下文信息,之前的 vDOM tree 显然难以满足,所以扩展出了 fiber tree(即 Fiber 上下文的 vDOM tree),更新过程就是根据输入数据以及现有的 fiber tree 构造出新的 fiber tree(workInProgress tree)。因此,Instance 层新增了这些实例:

关于 Fiber 需要知道的基础知识

  1. 浏览器刷新率(帧)

浏览器一帧做了什么
  1. 接受输入事件

  2. 执行事件回调

  3. 开始一帧

  4. 执行 RAF(requestAnimationFrame)

  5. 页面布局,样式计算

  6. 渲染

  7. 执行 RIC(requestIdleCallback)

第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。

JS 原生执行栈

在 react fiber 出现之前,react 通过原生执行栈递归遍历 VDOM。当浏览器引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并将其压入执行栈,如此往复。

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

浏览器得不到控制权,就不能及时开始下一帧的绘制。如果这个时间超过 16ms,当页面有动画效果需求时,动画因为浏览器不能及时绘制下一帧,这时动画就会出现卡顿,不仅如此,因为事件响应代码是在每一帧开始的时候执行,如果不能及时绘制下一帧,事件响应也会延迟

时间分片

时间分片指的是一种将多个粒度小的任务放入一个时间切片(一帧)中执行的一种方案,在 React Fiber 中就是将多个任务放在了一个时间片中去执行

链表

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

使用多向链表的形式替代了原来的树结构

  • 副作用单链表

  • 状态更新单链表

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

更新可控主要提现在这几方面 1.任务拆分 2.任务挂起、恢复、终止 3.任务具备优先级

1.任务拆分 React Fiber 之前是基于原生执行栈,每一次更新操作会一直占用主线程,直到更新完成。这可能会导致事件响应延迟,动画卡顿等现象。

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

import React from "react";
import ReactDom from "react-dom";
const jsx = (
  <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>
);
ReactDom.render(jsx, document.getElementById("root"));

这个组件在渲染的时候会被分成八个小任务,每个任务用来分别处理 A1(div)、A1(text)、B1(div)、B1(text)、C1(div)、C1(text)、C2(div)、C2(text)、B2(div)、B2(text)。再通过时间分片,在一个时间片中执行一个或者多个任务。这里提一下,所有的小任务并不是一次性被切分完成,而是处理当前任务的时候生成下一个任务,如果没有下一个任务生成了,就代表本次渲染的 Diff 操作完成。

2.挂起、、恢复、终止 react 有两颗 Fiber 树,workInProgress tree 和 currentFiber tree。

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

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

Fiber 请求调度

Fiber 架构的核心特性

  1. 增量渲染

    Fiber 将渲染任务拆分为多个小任务,可以分批次执行,避免一次性占用主线程。

  2. 任务优先级:

    Fiber 支持为不同的更新任务分配优先级(如用户交互、动画、数据更新等),确保高优先级任务优先执行。

  3. 并发模式(Concurrent Mode):

    Fiber 是 React 并发模式的基础,支持并发渲染和异步更新。

  4. 错误边界:

    Fiber 引入了错误边界(Error Boundaries),可以捕获组件树中的错误,避免整个应用崩溃。

Fiber 渲染过程分为两个阶段

  1. Reconciliation 阶段(协调阶段):

    • 在这个阶段,React 会遍历 Fiber 树,找出需要更新的节点,并生成一个副作用列表(Effect List)。

    • 这个阶段是可中断的,React 可以根据优先级暂停或恢复任务。

  2. Commit 阶段(提交阶段):

    • 在这个阶段,React 会一次性将所有的更新应用到 DOM 上。

    • 这个阶段是不可中断的,确保 DOM 更新的原子性。


上图是一个简易的整个 react 初始化的流程,在开始实现 useRef​ 前,有必要先来梳理一下 react 整个执行流程。

​render​

由于使用 jsx 由 babel 处理后的数据结构并不是真正的 dom 节点,而是 element​ 结构,一种拥有标记及子节点列表的对象,所以在拿到 element​ 对象后,首先要转化为 fiber​ 节点。

在 fiber​ 架构中,更新操作发生时,react 会存在两棵 fiber​ 树(current​ 树和 workInProgress​ 树),current​ 树是上次更新生成的 fiber​ 树(初始化阶段为 null),workInProgress​ 树是本次更新需要生成的 fiber​ 树。双缓存树的机制是判断当前处于那个阶段(初始化/更新),复用节点属性的重要依据。在本次更新完成后两棵树会互相转换。 ​render​ 阶段实际上是在内存中构建一棵新的 fiber​ 树(称为 workInProgress​ 树),构建过程是依照现有 fiber​ 树(current​ 树)从 root​ 开始深度优先遍历再回溯到 root​ 的过程,这个过程中每个 fiber​ 节点都会经历两个阶段:beginWork​ 和 completeWork​。 ​beginWork​ 是向下调和的过程。就是由 fiberRoot​ 按照 child 指针逐层向下调和,而 completeWork​ 是向上归并的过程,如果有兄弟节点,会返回 sibling​(同级)兄弟,没有返回 return​ 父级,一直返回到 FiebrRoot​。 组件的状态计算、diff​ 的操作以及 render​ 函数的执行,发生在 beginWork​ 阶段,effect​ 链表的收集、被跳过的优先级的收集,发生在 completeWork​ 阶段。构建 workInProgress​ 树的过程中会有一个 workInProgress​ 的指针记录下当前构建到哪个 fiber​ 节点,这是 react 更新任务可恢复的重要原因之一。 ‍

​commit​

在 render​ 阶段结束后,会进入 commi​t 阶段,该阶段不可中断,主要是去依据 workInProgress​ 树中有变化的那些节点(render​ 阶段的 completeWork​ 过程收集到的 effect​ 链表),去完成 DOM 操作,将更新应用到页面上,除此之外,还会异步调度 useEffect​ 以及同步执行 useLayoutEffect​。 ​commit​ 细分可以分为三个阶段:

​Before mutation​ 阶段:执行 DOM 操作前

没修改真实的 DOM ,是获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate​,会在这里执行。

​mutation​ 阶段:执行 DOM 操作

对新增元素,更新元素,删除元素。进行真实的 DOM 操作。

​layout​ 阶段:执行 DOM 操作后

DOM 已经更新完毕。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后更新于

这有帮助吗?