关于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)
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理