概述

React 16 之前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿, 非常的影响用户体验。

Fiber就是React提出的用于解决页面卡顿的方案,包含如下三个方面:

  1. 利用浏览器的空闲时间执行任务,不会长时间占用主线程。
  2. 因为利用了空闲时间执行任务,所以任务需要可以被随时中断,而迭代是无法中断的,循环是随时可以中断的,因此用循环替代迭代。
  3. 将对比更新操作拆分成一个个小的任务。

核心API

requestIdleCallback 是浏览器提供的API,其利用浏览器的空闲时间执行任务,如果有更高优先级的任务需要执行时,当前执行的任务可会被终止,优先执行更高优先级的任务。

requestIdleCallback接受一个函数作为参数,该函数是要执行的任务:

requestIdleCallback(function(deadline) {
  // deadline.timeRemaining() 获取浏览器的空余时间
})

API示例

在下面的html实例中,页面上包含两个按钮,点击第一个按钮执行一段耗时操作,点击另一个按钮alert显示一段内容:

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Page Title</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
</head>

<body>
    <button id="work">long time work</button>
    <button id="interaction">another work</button>
</body>
<script>
    var workBtn = document.getElementById("work")
    var interactionBtn = document.getElementById("interaction")
    var iterationCount = 100000000
    var value = 0

    // 模拟一段耗时操作
    workBtn.addEventListener("click", function () {
        while (iterationCount > 0) {
            value =
                Math.random() < 0.5 ? value + Math.random() : value + Math.random()
            iterationCount = iterationCount - 1
        }
    })

    interactionBtn.addEventListener("click", function () {
        alert('done another work')
    })
</script>

</html>

当点击第一个按钮之后迅速点击第二个按钮,会发现页面会卡顿一段时间之后才执行alert。

当用requestIdleCallback API改造之后:

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Page Title</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
</head>

<body>
    <button id="work">long time work</button>
    <button id="interaction">another work</button>
</body>
<script>
    var workBtn = document.getElementById("work")
    var interactionBtn = document.getElementById("interaction")
    var iterationCount = 100000000
    var value = 0

    // 模拟一段耗时操作
    var expensiveCalculation = function (IdleDeadline) {
        // 空闲时间超过1秒才执行
        while (iterationCount > 0 && IdleDeadline.timeRemaining() > 1) {
            value =
                Math.random() < 0.5 ? value + Math.random() : value + Math.random()
            iterationCount = iterationCount - 1
        }
        requestIdleCallback(expensiveCalculation)
    }

    workBtn.addEventListener("click", function () {
        requestIdleCallback(expensiveCalculation)
    })

    interactionBtn.addEventListener("click", function () {
        alert('done another work')
    })
</script>

</html>

再次快速点击第一个按钮和第二个按钮,会发现,页面迅速alert一段信息,说明第一个任务并没有阻塞第二个任务。

思路

Fiber将Dom对比算法分解成两步:

  1. 构建Fiber对象(可理解成一个个小的任务对象),这个过程可以随时被中断。
  2. 提交:将Fiber对象渲染成真实Dom,这个过程是不可以被中断的。

Fiber对象

为了能够模拟实现整个Fiber的核心代码,需要首先了解Fiber对象的结构,Fiber对象是一个普通的js对象,其包含如下属性:

属性名说明
type节点类型,和虚拟Dom对象的type相同,用于区分元素、文本、组件
props节点属性,同虚拟Dom对象
stateNode节点Dom对象或者组件实例
tag标记,用于标记节点
effects存储包含自身和所有后代的Fiber数组
effectTag标记当前节点需要进行的操作,包含插入、更新、移除等
parent父Fiber对象,在React源码中叫Return
child当前Fiber对象的子级Fiber对象
sibling当前Fiber对象的下一级兄弟节点
alternateFiber对象备份,用于对比

最终虚拟Dom树会被转换成Fiber对象的树形结构数据,最顶层的节点effects属性中包含了该树结构所有的Fiber对象,其是一个数组,也就是前文说的能被中断的一个个小任务的任务操作对象。

模拟任务队列

本节将模拟实现一个任务队列,该任务队列将为后续Fiber执行提供基础,当浏览器有空闲时间时,会不断的从任务队列中取出任务并执行。

任务队列:

const createTaskQueue = () => {
  const taskQueue = []
  return {
    /**
     * 向任务队列中添加任务
     */
    push: item => taskQueue.push(item),
    /**
     * 从任务队列中获取任务
     */
    pop: () => taskQueue.shift(),
    /**
     * 判断任务队列中是否还有任务
     */
    isEmpty: () => taskQueue.length === 0
  }
}

export default createTaskQueue

实现空闲时间任务调度

render方法是节点挂载的入口方法,当调用render的时候,会执行首次渲染或者对比更新,因此需要修改render方法。利用浏览器提供的api实现空闲执行。

// 创建任务队列
const taskQueue = createTaskQueue()

// 空闲时间执行的具体方法
const performTask = deadline => {
  // 执行任务,workLoop方法后续补充
  workLoop(deadline)
  // 实现持续调用
  if (subTask || !taskQueue.isEmpty()) {
    requestIdleCallback(performTask)
  }
}

// 暴露的render方法
export const render = (element, dom) => {
  // 1. 添加任务 =》 构建fiber对象
  taskQueue.push({
    dom,
    props: { children: element }
  })
  // 2. 指定浏览器空闲时间执行performTask 方法
  requestIdleCallback(performTask)
}

carry
58 声望7 粉丝

学无止境