React源码解析(二):Fiber架构

1,633 阅读4分钟

前言

本篇是React源码解析系列第二篇。主要学习React的Fiber架构。源码版本为v18.2.0。

为什么需要Fiber?

在老的React架构中,每次更新会导致React从根节点开始协调,同步递归dom-diff去计算新的虚拟DOM树,直到提交真实DOM修改,这就是Stack Reconciler,依赖于内置堆栈来遍历,它会一直工作,直到堆栈为空,如果dom节点过多,会导致点击、动画、布局渲染等事件滞后,造成卡顿。
因此,新的 Fiber Reconciler 应运而生。

何为Fiber?

Fiber可以理解为将一个庞大的任务进行了任务切片,分割成一个个小任务,将协调过程变为可中断,在超出可执行时间后及时让渡控制权给浏览器,去执行其他任务,避免阻塞,可以让浏览器及时地响应用户的交互。以下是函数的调用堆栈,第一个是 Stack Reconciler, 第二个是 Fiber Reconciler

image.png

浏览器刷新频率与帧

大多数的屏幕刷新率都为60HZ,也就是每秒会刷新60次,每次刷新为一帧,那么一帧的时长大概就是1000ms/60=16.6ms。
每帧执行的任务顺序都是固定的,如下图所示: image.png 可以看出来,在每帧会先去执行JS任务再去进行页面布局和绘制,我们知道Javascript 引擎和页面渲染引擎是在同一个渲染线程,且GUI 渲染和 Javascript 执行两者是互斥的,如果某个任务执行时间过长(超过16.6ms),那么本帧便不会进行渲染,推迟到下一帧进行。页面就会表现为卡顿,或者说掉帧

每帧执行完成后还可能会剩余一些时间作为空闲时间,此时会去执行requestIdleCallback,这个API 使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件。当然,React源码中并未使用这个API去做任务调度,因为它的浏览器兼容性比较差。

Fiber是数据结构

类似于虚拟DOM,每个React元素也会对应有一个Fiber单元,它们会组成一个Fiber树。
在React中,这棵树是使用链表连接而成,每个Fiber节点都会有以下几个指针指向固定的其他节点:

  1. child: 指向自己的第一个孩子
  2. sibling: 指向自己的第一个弟弟
  3. return: 指向自己的父节点

如以下DOM节点:

function App() {
  return (
    <div>
      <h1>
        <p></p>
        <a></a>
      </h1>
      <h2></h2>
    </div>
  );
}

转换为Fiber结构后

image.png

Fiber树的执行顺序

Fiber树的执行顺序是深度遍历,具体执行如下:

  1. 根节点开始遍历。
  2. 有孩子则遍历第一个孩子
  3. 没有孩子则表示此节点已经遍历完成,遍历此节点的第一个弟弟
  4. 如果此节点既没有孩子也没有弟弟,则返回父节点,表示父节点遍历完毕,开始遍历父节点的第一个弟弟
  5. 最后回到根节点,遍历完成。

Fiber作为执行单元

每个Fiber都是一个执行单元,每次执行完一个单元后React会去检查是否超出控制时间,如果超出则会停止任务执行。

image.png

Fiber的双缓冲机制

首先先提一下React渲染机制,React的渲染分为两个阶段:RenderCommit,其中 Render 阶段会去做如dom-diff、优先级调度等等任务,最后会得到一个新的Fiber树,这个过程是可以中断的,而Commit阶段则是去修改真实DOM,它是同步的,必须一气呵成完成,不可中断。因此在 Render 阶段,必须要保持浏览器页面不变直到 Render 阶段完成,也就是说我们在 Render 阶段需要保持当前Fiber树不变,然后用另一棵树来作为更新后的Fiber树,最后新树直接替换掉旧树。这就是双缓冲
简单来说就是React在 Render 阶段会用当前的Fiber节点去创建一个替身(alternate),替身节点组成一颗替身树,用这棵替身树去做更新,更新完毕后直接用替身树替换掉当前树,同时重用老节点,删除或更新新结点(dom-diff)。

image.png

image.png

至此,我们了解了Fiber的基础概念。

本文正在参加「金石计划」