Skip to content

为什么需要Fiber架构

在React 16之前,React使用的是基于堆栈的递归调和算法(dom-diff),这种算法在进行虚拟DOM比较时可能会阻塞主线程,导致页面渲染卡顿,用户体验不佳。为了解决这个问题,React团队引入了Fiber架构。为了让大家有直观的感受引入Fiber架构,可以看下这样的一个例子:

js
// https://claudiopro.github.io/react-fiber-vs-stack-demo/fiber.html
// https://claudiopro.github.io/react-fiber-vs-stack-demo/stack.html

我们前面所实现的原始版本的React源码其实就是基于堆栈,所谓基于堆栈,其实就是正常的函数调用,直到函数逐个调用完成,中间没有停顿一气呵成。我们在原始版本实现的代码可以这样来示意:

js
function updateDomTree(parent, children) {
  // 遍历子组件
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    // 创建或更新 DOM 节点
    updateDOMNode(parent, child);
    // 递归调和子组件的子组件
    if (child.children && child.children.length > 0) {
      updateDomTree(child, child.children);
    }
  }
}

// 更新整个组件树
updateDomTree(root, root.children);

在这个简化的示例中,updateDomTree 函数递归地遍历整个组件树,直到所有子组件都完成调和。由于 JavaScript 是单线程的,这意味着在调和过程中,其他任务(如动画、用户交互等)将被阻塞。在进一步解释为什么会被阻塞之前,我们先来看一张流程图来表示浏览器做了哪些工作:

js
+-----------------------------------------------------------+
|    JavaScript Execution                                   |
|   (Event Handling, Animation, AJAX, DOM Manipulation)     |
+-----------------------------------------------------------+
                              |
                              v
+-----------------------------------------------------------+
|    Style Recalculation                                    |
|   (Update CSSOM, Resolve Inheritance, Cascade Styles)     |
+-----------------------------------------------------------+
                              |
                              v
+-----------------------------------------------------------+
|    Layout Update                                          |
|   (Generate Layout Tree, Calculate Positions and Sizes)   |
+-----------------------------------------------------------+
                              |
                              v
+-----------------------------------------------------------+
|    Paint & Rasterization                                  |
|   (Draw Elements, Rasterize Text, Create Layers)          |
+-----------------------------------------------------------+
                              |
                              v
+-----------------------------------------------------------+
|    Compositing                                            |
|   (Layer Management, Blending, GPU Acceleration)          |
+-----------------------------------------------------------+
                              |
                              v
+-----------------------------------------------------------+
|    Display on Screen                                      |
|   (Update Screen, V-Sync, Refresh Rate)                   |
+-----------------------------------------------------------+
  • JavaScript Execution(JavaScript 执行):在一帧的开始,浏览器执行 JavaScript 代码。这包括处理用户交互事件(如点击、滚动和触摸)、动画更新、数据获取(如 AJAX 请求)、以及直接操作 DOM。
  • Style Recalculation(样式重新计算):如果 JavaScript 操作导致了样式更改,浏览器将重新计算受影响元素的样式。这包括更新 CSSOM、解析样式继承、以及级联样式规则。
  • Layout Update(布局更新):样式更改可能导致布局信息的更新。浏览器将生成布局树并计算元素的新位置和大小。
  • Paint & Rasterization(绘制和光栅化):浏览器根据更新后的布局信息将元素绘制到屏幕上。这包括绘制矢量图形、文本、图像等,并将矢量图形转换为位图(光栅化)。此过程还包括创建图层,为合成阶段做准备。
  • Compositing(合成):浏览器将各个图层合成为最终的图像。这个过程涉及图层管理、图像合成和混合。此外,合成阶段还可以利用 GPU 加速以提高性能。
  • Display on Screen(显示在屏幕上):浏览器将生成的最终图像显示在屏幕上。这涉及更新屏幕、与显示器的垂直同步信号(V-Sync)保持同步,以及适应屏幕的刷新率。

关于浏览器的工作原理,也可以请大家参考一篇文章丰富理论基础知识https://developer.chrome.com/blog/inside-browser-part3/

反正浏览器做的事情是很多的,但是这里存在一个问题,问题就是为了实现流畅的用户体验,浏览器需要在每秒约 60 帧的速率下完成这些任务。这意味着每一帧只有大约 16.7 毫秒的时间来执行所有操作,所以当我们一个函数调用栈持续时间过长的时候,会给用户卡顿的感觉。

那么既然浏览器会做很多事情,那就有轻重缓急,什么是着急的事情呢?就是那些不处理就会降低用户体验的事情,比如我点击一个按钮没有反应,动画卡顿等等这些都是难以接受的。浏览器需要处理的事情很多,所以一些不特别重要的工作我们就希望当浏览器不忙的时候(所谓不忙就是没有紧急的事情需要处理)再执行,但是怎么知道浏览器忙不忙呢?这就要提到一api,叫做requestIdleCallback

requestIdleCallback 是一个浏览器提供的 API,它允许你在浏览器的空闲时段执行一些后台或低优先级任务。当浏览器主线程空闲时,即在处理高优先级任务(如动画、用户交互、布局更新等)之间的间隙,requestIdleCallback 会调用你提供的回调函数。它可以帮助你有效地利用主线程的闲置时间,以提高应用程序的性能和响应速度。

requestIdleCallback 的工作原理如下:

  1. 注册回调函数:你可以使用 requestIdleCallback 函数向浏览器注册一个回调函数。浏览器会在有空闲时间时尽快调用这个回调函数。
js
requestIdleCallback(myCallback);
  1. 回调函数参数:当浏览器调用你的回调函数时,它会传递一个 IdleDeadline 对象作为参数。这个对象包含以下方法:
  • timeRemaining(): 返回当前空闲周期中剩余的时间,以毫秒为单位。你可以使用这个方法来检查浏览器还有多少空闲时间可以执行你的任务。
  • didTimeout: 一个布尔值,表示回调是否因为超时而被调用。如果你在注册回调时设置了超时,那么当超时发生时,这个属性会为 true。
  1. 设置超时:在注册回调函数时,你可以设置一个可选的超时参数。如果在超时时间内浏览器仍未找到空闲时间来执行回调,那么它会尽快调用回调函数,并将 IdleDeadline 对象的 didTimeout 属性设置为 true。
js
requestIdleCallback(myCallback, { timeout: 1000 }); // 设置超时为 1000 毫秒
  1. 取消回调:如果你需要取消之前注册的回调函数,可以使用 cancelIdleCallback 函数。它接受一个表示回调 ID 的参数,这个 ID 由 requestIdleCallback 返回。
js
const callbackId = requestIdleCallback(myCallback);
cancelIdleCallback(callbackId); // 取消回调

总之,requestIdleCallback 提供了一种在浏览器空闲时执行低优先级任务的方法,有助于提高应用程序的性能和响应速度。你可以利用 IdleDeadline 对象中的信息来有效地调度和执行任务。

虽然 requestIdleCallback 是一个很有用的工具,可以帮助我们在浏览器空闲时执行低优先级任务,但它也有一些局限性:

  1. 浏览器支持:requestIdleCallback 目前并非所有浏览器都支持。尤其是在一些较旧版本的浏览器中,它可能不可用。在使用它之前,你需要确保对目标浏览器进行兼容性检查。
  2. 不可预测的执行时间:requestIdleCallback 只在浏览器空闲时执行回调函数,这意味着你不能确切地知道回调函数会在何时执行。这可能会导致一些任务被延迟执行,尤其是在浏览器主线程忙碌的情况下。
  3. 不适用于实时性要求高的任务:由于执行时间不可预测,requestIdleCallback 不适合用于对实时性要求较高的任务,如动画、用户交互和即时响应。
  4. 可能的性能影响:尽管 requestIdleCallback 的目的是在空闲时间执行任务以提高性能,但如果你在回调函数中执行了高耗时或高计算量的任务,仍然可能对性能产生负面影响。
  5. 没有设置优先级:requestIdleCallback 不允许为回调函数设置优先级。这意味着所有使用 requestIdleCallback 注册的回调函数具有相同的优先级,你需要在应用程序逻辑中自行处理这些回调的优先级。
  6. 在 Web Worker 中的局限性:虽然 requestIdleCallback 可以在 Web Worker 中使用,但在这种情况下,它的优势并不明显。因为 Web Worker 本身就是在后台执行任务,不会阻塞主线程,因此 requestIdleCallback 在 Web Worker 中的使用场景有限。

关于requestIdleCallback,可以参阅https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback

总之,requestIdleCallback 是一个有用的工具,可以帮助你在浏览器空闲时执行低优先级任务。然而,它并非适用于所有场景,由于他的这些局限性,我们在React18中并没有通过这种方式来实现利用浏览器的空余时间,而是在内部使用了别的方式。但是我们这里,还是使用requestIdleCallback来举一个例子:

js
const tasks = [
  () => console.log("Task 1"),
  () => console.log("Task 2"),
  () => console.log("Task 3"),
  () => console.log("Task 4"),
  () => console.log("Task 5"),
];

function processTask(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
    const task = tasks.shift(); // 从任务队列中取出一个任务
    task(); // 执行任务
  }

  if (tasks.length > 0) {
    // 如果仍有未完成的任务,则继续请求空闲回调
    requestIdleCallback(processTask);
  } else {
    console.log("All tasks completed!");
  }
}

// 请求空闲回调以启动工作循环
requestIdleCallback(processTask);

要想利用浏览器的空余时间来工作,有一个前提要求,那就是任务是一个个独立的任务。而我们引入Fiber架构的原因,就是为了将我们原本一气呵成的堆栈调用进行拆分,拆分成一个个可以独立运行的任务,充分利用浏览器的工作机制,提升性能。

现在我们知道了Fiber架构很重要,对性能而言有很大的改进,那究竟什么是Fiber架构呢?下一小节进行介绍。

基于 VitePress 构建