mini-react新版本fiber架构

以前写了一篇stack版的mini-react实现,这里再写一篇fiber版的实现。

这里若是不知道二者的区别的话,推荐先看看我这一篇文章:
stack和fiber架构的区别css

从我上面链接这篇文章咱们能够知道:React 16 以前的版本比对更新 VirtualDOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就没法中断,若是应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成以后主线程才能被释放,主线程才能执行其余任务。这就会致使一些用户交互,动画等任务没法当即获得执行,页面就会产生卡顿, 很是的影响用户体验。 html

其主要问题是:递归没法中断,执行重任务耗时长。 JavaScript 又是单线程,没法同时执行其余任务,致使任务延迟页面卡顿,用户体验差。node

咱们得解决方案是:react

  1. 利用浏览器空闲时间执行任务,拒绝长时间占用主线程
  2. 放弃递归只采用循环,由于循环能够被中断
  3. 任务拆分,将任务拆分红一个个的小任务

基于以上几点,在这里咱们先了解下requestIdleCallback这个api算法

核心 API 功能介绍:利用浏览器的空余时间执行任务,若是有更高优先级的任务要执行时,当前执行的任务能够被终止,优先执行高级别任务。segmentfault

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

这里咱们了解下什么是浏览器空余时间:页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面是流畅的,小于这个值时, 用户会感受到卡顿,1s 60帧,每一帧分到的时间是 1000/60 ≈ 16 ms,若是每一帧执行的时间小于16ms,就说明浏览器有空余时间。api

若是任务在剩余的时间内没有完成则会中止任务执行,继续优先执行主任务,也就是说 requestIdleCallback 老是利用浏览器的空余时间执行任务。数组

咱们先用这个api作个例子,来看:
html浏览器

<div class="playground" id="play">playground</div>
<button id="work">start work</button>
<button id="interaction">handle some user interaction</button>

css架构

<style>
  .playground {
    background: palevioletred;
    padding: 20px;
    margin-bottom: 10px;
  }
</style>

js

var play = document.getElementById("play")
var workBtn = document.getElementById("work")
var interactionBtn = document.getElementById("interaction")
var iterationCount = 100000000
var value = 0

var expensiveCalculation = function (IdleDeadline) {
  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 () {
  play.style.background = "palegreen"
})

从这个示例中咱们知道了,这个api该如何使用,该如何中断任务。

而后咱们来实现咱们得fiber架构得react-mini版本。

在 Fiber 方案中,为了实现任务的终止再继续,DOM比对算法被分红了两部分:

  1. 构建 Fiber (可中断)
  2. 提交 Commit (不可中断,更新dom)

目前咱们设计得fiber对象有如下属性:

{
  type         节点类型 (元素, 文本, 组件)(具体的类型)
  props        节点属性
  stateNode    节点 DOM 对象 | 组件实例对象
  tag          节点标记 (对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
  effects      数组, 存储须要更改的 fiber 对象
  effectTag    当前 Fiber 要被执行的操做 (新增, 删除, 修改)
  parent       当前 Fiber 的父级 Fiber
  child        当前 Fiber 的子级 Fiber
  sibling      当前 Fiber 的下一个兄弟 Fiber
  alternate    Fiber 备份 fiber 比对时使用
}

image.png

这时咱们的项目结构

react/index.js 是入口文件,在这里咱们对它作react主要使用api的导出

import createElement from "./CreateElement"
export { render } from "./reconciliation"
export { Component } from "./Component"

export default {
  createElement
}

而后咱们先来写createElement 用来把jsx生成 vnode的函数:
react/CreateElement

export default function createElement(type, props, ...children) {
  const childElements = [].concat(...children).reduce((result, child) => {
    if (child !== false && child !== true && child !== null) {
      if (child instanceof Object) {
        result.push(child)
      } else {
        result.push(createElement("text", { textContent: child }))
      }
    }
    return result
  }, [])
  return {
    type,
    props: Object.assign({ children: childElements }, props)
  }
}

这个的实现是和stack架构同样的

接下来就是咱们的render函数,reconciliation/index.js 这里也就是咱们核心的协调算法了.

在这里咱们主要工做是分两个阶段:
1.根据vnode来生成fiber对象,生成fiber的过程是经过循环来生成的,由于咱们生成fiber以及它的子集fiber的过程是能够被打断的,因此咱们经过循环的方式来生成,也就是说咱们如今的fiber的结构是一个链表的结构.
image.png

这是一个树的结构,在咱们生成的fiber对象中,会有parent指向父节点,child指向咱们当前子集的第一个的节点,也就是最左侧的节点,子集中其余节点是咱们这个child的兄弟节点sibling。

每次构建一个子集就是一个子任务,fiber任务是能够被打断的。

2.咱们每次构建fiber会把当前fiber以及自身收集到的子集fiber的effects数组放入父级的effects数组中,而后第二个阶段是commit生成dom阶段,咱们只须要循环最外层的fiber中effects数组就能够,这个数组中放着全部的fiber对象,咱们只须要依次把它们的stateNode 也就是dom对象根据effectTag操做符,来对比更新,删除新增到它的parent父级的stateNode中.

这样咱们一个完整流程就完成了.

相关文章
相关标签/搜索