fiber架构让react并发更新成为可能

613 阅读8分钟

什么是fiber

Fiber 是 React 内部的一个核心算法,用于协调组件的渲染和生命周期。它是 React 团队为了解决传统 React 渲染模型(基于栈的递归)的局限性而设计的。Fiber对React核心算法进行重构,采用全新的数据结构来提高页面的响应速度。

引入fiber之前

在介绍fiber前先给大家介绍介绍在引入fiber之前react开发遇到了什么问题,下面我将通过一段简单的代码来给大家展示react 15之前的渲染过程。

我们知道虚拟dom就是一个对象,这个对象包含了dom节点类型的type,以及dom属性的props对象,假如我们现在有以下dom对象,我们需要通过方法给它渲染到页面。

const vDom = {
  type: "div",
  props: {
    id: "0",
    children: [
      {
        type: "div",
        children: 1,
      },
    ],
  },
};

将虚拟dom渲染在页面上一共分为以下几个过程:创建dom,加工属性,递归遍历子元素,如下代码示例:

const render = (element, container) => {
  // 创建dom节点
  let dom = document.createElement(element.type);
  // 添加属性
  const props = Object.keys(element.props);
  props.forEach((e) => {
    if (e !== "children") {
      dom[e] = element.props[e];
    }
  });
  // 处理子元素
  if (Array.isArray(element.props.children)) {
    // 是数组,那就继续递归
    element.props.children.forEach((c) => render(c, dom));
  } else {
    // 是文本节点就设置文本
    dom.innerHTML = element.props.children;
  }
  // 将当前加工好的dom节点添加到父容器节点中
  container.appendChild(dom);
};

render(vDom, document.getElementById("root"));

我们可以发现在该过程中,老版本react是用递归的方式进行渲染,那么一旦该dom的结构非常复杂那么该递归的过程将会非常耗时,而且一旦该递归一开始那么将不能停止。

相信大家都对浏览器的渲染非常熟悉,浏览器的JS引擎线程UI渲染线程是互斥的,假如上述递归的过程耗时足够久那么浏览器就得一直等待,这样将会造成浏览器的渲染严重卡顿极大的影响用户体验。

为了解决以上问题,react团队在react16引入了一种全新的解决方案fiber架构,虽然我们现在还不了解fiber架构的具体实现原理。

但是我们知道浏览器的JS引擎线程UI渲染线程是互斥的,也就是说我们在每一帧中执行完了js代码浏览器的渲染后可能还会存在空闲时间,那么我们就可以利用这段空闲时间来完成相应的任务。

image.png

fiber原理

在浏览器中,网页内容的显示是通过连续的帧来实现的,每一帧的生成与显示器的刷新周期同步。通常情况下,显示器的刷新率是每秒钟60次,这意味着每秒钟会刷新60次屏幕。当页面每秒渲染的帧数(FPS)达到或超过60帧时,用户会感觉到页面的动画和滚动是平滑的;相反,如果FPS低于60帧,用户可能会察觉到画面的卡顿。那么,在构成流畅画面的每一帧中,浏览器究竟完成了哪些步骤呢?让我们来详细了解一下。

image.png (图片来源:网络)

  1. 响应用户操作:首先,浏览器得确保用户点击、滚动或输入等操作能迅速得到反馈,这样用户就觉得网页是灵敏的。

  2. 检查计时器:然后,浏览器会看看有没有设置好的计时器到了该响铃的时候,如果有,就得执行那些计时器里的代码。

  3. 处理帧事件:接下来,浏览器要处理一些与帧相关的事件,比如窗口大小变化、滚动页面或者媒体查询的变化。

  4. 动画帧回调:在每次屏幕要刷新前,浏览器会执行那些通过requestAnimationFrame设置的动画回调函数,这样可以确保动画的流畅。

  5. 布局计算:然后,浏览器要计算网页上每个元素的位置和大小,确定它们在屏幕上该如何排列。

  6. 绘制内容:布局确定后,浏览器开始绘制每个元素,填充颜色和文字,让它们在屏幕上显示出来。

  7. 空闲时处理:当这些都做完后,如果还有空闲时间,浏览器会利用这段时间执行那些不那么紧急的任务,这些任务是通过requestIdleCallback注册的,这个功能后面会详细讲,它是让网页保持流畅的关键技术之一。

在前面我们提到了react 15以及之前的版本渲染/更新的过程是采用递归的方式深度遍历这种方式势必会造成主线程长时间占用,为了解决这个问题于是引入了Fiber来改变这种不可控的现状。

浏览器通过细分渲染和更新过程,利用智能的时间管理来控制每个小任务的执行。这样做可以减少页面出现卡顿的可能性,让用户体验更加流畅。React的Fiber架构让更新过程可以暂停和恢复,这样浏览器就能在关键时刻让出CPU资源,确保用户的操作能够得到即时响应。简而言之,Fiber架构让浏览器在处理网页更新时更加灵活,确保用户操作的优先级,提升整体的浏览体验。

什么是fiber

fiber可以理解为是一个执行单元,也可以理解为是一种链表数据结构。

执行单元

可以把fiber理解成一个执行单元,每执行完一个执行单元,react就会检查现在还剩余多少时间,如果还有空闲时间则将时间控制权交出去。这个获取控制权的过程可以使用浏览器的requestIdleCallback方法实现,该方法会在后面介绍。

Fiber架构就像是将一个大任务分解成一系列小任务的智能管家。每个小任务都是一个独立的单元,需要一口气完成,不能中途停下来。但是,一旦一个小任务完成了,Fiber就会主动把控制权交还给浏览器,让浏览器有机会去处理用户的新操作。这样,就不需要像以前那样,非得等到整个大任务全部执行完毕后,浏览器才能响应用户的新动作。简而言之,Fiber通过细分任务,让浏览器在处理任务的同时,也能保持对用户操作的快速响应。

image.png (图片来源:网络)

数据结构

Fiber使用链表这种形式来组织。在这个结构中,每个虚拟DOM元素都对应一个Fiber节点,就像下面的图示那样,每个节点都代表一个Fiber。每个Fiber节点都有指向它的孩子(child)、兄弟(sibling)和父母(parent)的指针。React Fiber的工作机制,就是基于这种链表结构来实现的。接下来的内容会详细解释,基于这种链表结构,Fiber是如何运作的。简而言之,Fiber通过这种链表结构,为React的渲染和更新提供了强大的支持。

image.png

requestIdleCallback获取空闲时间的控制权

在fiber中使用了requestIdleCallback方法来获取每一帧的空闲时间的控制权。

在空闲阶段时,可以执行 requestIdleCallback 里注册的任务,requestIdleCallback 接受了两个参数:

window.requestIdleCallback(callback, { timeout: 1000 })

requestIdleCallback方法注册对应的任务,告诉浏览器我的这个任务优先级不高,如果每一帧内存在空闲时间,就可以执行注册的这个任务。另外,开发者是可以传入timeout参数去定义超时时间的,如果到了超时时间了,浏览器必须立即执行,使用方法如下:

// 该函数的执行时间超过1s
function calc() {
  let start = performance.now();
  let sum = 0;
  for (let i = 0; i < 10000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random();
    }
  }
  let end = performance.now();
  let totolTime = end - start;
  // 得到该函数的计算用时
  console.log(totolTime, "totolTime");
}
 
let tasks = [
  () => {
    calc();
    console.log(1);
  },
  () => {
    calc();
    console.log(2);
  },
  () => {
    console.log(3);
  }
];
 
let work = deadline => {
  console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);
 
  // 如果此帧剩余时间大于0,或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
  // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    tasks.length > 0
  ) {
    let fn = tasks.shift();
    fn();
  }
  // 如果还有未完成的任务,继续调用 requestIdleCallback 申请下一个时间片
  if (tasks.length > 0) {
    window.requestIdleCallback(work, { timeout: 500 });
  }
};
 
window.requestIdleCallback(work, { timeout: 500 });

运行结果如下:

image.png

requestAnimationFrame在每一帧到来时执行相应的回调

在Fiber中使用requestAnimationFrame这个浏览器的API,确保浏览器在下一次重绘之前调用相应的回调函数来更新动画,确保我们在上一帧所占用的空闲时间执行的js所造成的渲染能够正常显示。

requestAnimationFrame使用案例如下:

浏览器在每一帧中,将页面 div 元素的宽变长2px,直到宽度达到1000px停止

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .progress {
            background-color: green;
        }
    </style>
</head>

<body>
    <div id="div" class="progress"></div>
    <button id="start">开始</button>
</body>

<script>
    let btn = document.getElementById('start')
    let div = document.getElementById('div')
    let start = 0
    let allInterval = []

    const progress = () => {
        div.style.width = div.offsetWidth + 2 + 'px'
        div.innerHTML = (div.offsetWidth / 10) + '%'
        if (div.offsetWidth < 1000) {
            let current = Date.now()
            allInterval.push(current - start)
            start = current
            requestAnimationFrame(progress)
        } else {
            console.log(allInterval) 
        }
    }

    btn.addEventListener('click', () => {
        div.style.width = 0
        let currrent = Date.now()
        start = currrent
        requestAnimationFrame(progress)
        console.log(allInterval)
    })

</script>
</html>

在这段代码中,浏览器会在每一帧中将div的宽度变宽2px,直到1000px为止。

image.png

总结

以上内容便是我对Fiber的简单介绍,具体代码实现原理感兴趣的朋友可以结合React源码深入研究。

好了本篇文章到这就结束了,希望能对大家有所帮助,如果有所帮助,还请大家点赞、关注、收藏支持一波,谢谢大家。

image.png