乐趣区

关于javascript:React-fiber原理解析及自定义实现一

概述

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 对象的下一级兄弟节点
alternate Fiber 对象备份,用于比照

最终虚构 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)
}
退出移动版