mini-react新版本fiber架构

Charon

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

mini-react-fiber-git地址

这里如果不知道两者的区别的话,推荐先看看我这一篇文章:
stack和fiber架构的区别

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

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

我们得解决方案是:

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

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

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

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

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

如果任务在剩余的时间内没有完成则会停止任务执行,继续优先执行主任务,也就是说 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指向我们当前子集的第一个的节点(每个fiber都只有一个child),也就是最左侧的节点,子集(children)中其他节点是我们这个child的兄弟节点sibling。

每次构建一个子集就是一个子任务,fiber任务是可以被打断的。
其实每次整体构建fiber树 都是一个对虚拟dom树模仿递归的深度优先的一个循环遍历,我们生成的fiber树的过程是这样:

步骤1:首先模仿递归里递的操作时,也就是往里走的时候,会直接找当前fiber节点的子级(也就是children中第一个节点),找到之后的操作:

  • (然后会建立子级和这个子级与当前children里的其他节点的兄弟节点的关系sibling(每一个节点都是上一个节点的兄弟节点))。
    然后继续往里找子级 ,找到之后创重复上面所说的操作。

步骤2:找到最后 最下面了,已经没有子级了, 那就会找当前子级的兄弟fiber节点sibling,然后继续在当前这个兄弟节点上 重复我们上面的操作 继续从这个兄弟节点上来找子级 执行我们上面所说的步骤

步骤3:到这里 已经没有兄弟节点了,然后接下来 就会跳到当前节点的父级节点上,然后找父级节点的兄弟节点,找到之后 再进行找这个节点的子级(重复步骤1和步骤2 条件满足会走步骤3),跳到父级这一步其实是归的一个操作,我们持续这个操作 一直跳到当前节点 没有父级了,也就是说 跳到了最开始的根节点(只有根节点是没有父级的),也就证明我们本次构建结束了。

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

其次更新操作就是 根据组件实例上的fiber对象来找到根,重新从根root的fiber来开始重新构建fiber子集,然后在构建过程中来和旧的fiber对象做比对来确定当前生成fiber的effectTag操作类型(删除,新增,更新).

这样我们一个完整流程就完成了.
下面贴一下代码

import { updateNodeElement } from "../DOM"
import {
  createTaskQueue,
  arrified,
  createStateNode,
  getTag,
  getRoot
} from "../Misc"



/**
 * 任务队列
 */
const taskQueue = createTaskQueue()
/**
 * 要执行的子任务
 */
let subTask = null


let pendingCommit = null

/**
 * commit阶段,更新dom的函数,该过程不可被打断
 * @param {*} fiber 更新传入fiber的effects数组中的fiber
 */
const commitAllWork = fiber => { //commit阶段 更新dom
  console.log(fiber);
  /**
   * 循环 effets 数组 构建 DOM 节点树
   */
  fiber.effects.forEach(item => {
    if (item.tag === "class_component") { //如果这个fiber是 类组件类型时
      item.stateNode.__fiber = item  //给当前实例上储存下 fiber 做下相互引用,后续方便用来做state融合
    }

    if (item.effectTag === "delete") {// 删除操作,
      item.parent.stateNode.removeChild(item.stateNode) //直接删除
    } else if (item.effectTag === "update") { //更新操作
      /**
       * 更新
       */
      if (item.type === item.alternate.type) { //更新时每个节点都有alternate
        /**
         *  节点类型相同,更新属性
         */
        updateNodeElement(item.stateNode, item, item.alternate)
      } else {
        /**
         * 节点类型不同,直接更新节点
         */
        item.parent.stateNode.replaceChild(
          item.stateNode, //新的dom
          item.alternate.stateNode //旧的dom
        )
      }
    } else if (item.effectTag === "placement") { //初始渲染
      /**
      * 向页面中追加节点
      */
      /**
       * 当前要追加的子节点
       */
      let fiber = item
      /**
       * 当前要追加的子节点的父级fiber
       */
      let parentFiber = item.parent

      /**
       * 找到普通节点父级 排除组件父级
       * 因为组件父级是不能直接追加真实DOM节点的,组件得stateNode是组件实例,不是真实dom
       */
      while ( //父节fiber如果是 函数或者类标识的fiber时 不是有效的dom fiber
        parentFiber.tag === "class_component" ||
        parentFiber.tag === "function_component"
      ) {
        parentFiber = parentFiber.parent
      }

      /**
       * 如果子节点是普通节点 找到父级 将子节点追加到父级中
       * type是string  我们定义的普通类型时
       */

      if (fiber.tag === "host_component") {
        //把当前stateNode也就是储存的真实dom   插入到父级真实dom中
        parentFiber.stateNode.appendChild(fiber.stateNode)
      }
    }
  })

  /**
 * 每次更新完成,备份一下旧的 fiber 节点对象,后续更新对比使用
 * 在这里fiber会是rootfiber对象 顶级的fiber ,然后备份下fiber到 dom对象上 getFirstTask方法会用到
 * 因为每次是从头开始构建子任务,所以每次赋值一个顶级的旧的fiber就可以,每次会以这个拆分旧的往下分发子fiber
 */
  fiber.stateNode.__rootFiberContainer = fiber
}




/**
* 从任务队列中获取任务,来获取rootfiber 返回最外层的fiber对象
*/
const getFirstTask = () => {
  const task = taskQueue.pop() //从队列取出任务,先进先出,内部调用shift
  if (task.from === "class_component") { // 类组件setState时的任务
    console.log(getRoot)
    const root = getRoot(task.instance) //获取最外层的fiber 生成任务开始构建
    task.instance.__fiber.partialState = task.partialState //给组件实例上的_fiber储存一下新的状态 
    //后续构建类组件fiber时再合并
    return { //返回最外层的fiber,然后开始从头开始构建fiber生成子任务
      props: root.props,
      stateNode: root.stateNode,
      tag: "host_root",
      effects: [],
      child: null,
      alternate: root //这里的操作是setState,更新操作, 需要储存alternate 老的root,方便后续新旧fiber对比更新
    }
  }
  /**
   * 先处理首次渲染,返回最外层节点的fiber对象
   */
  return {
    props: task.props, //储存props
    stateNode: task.dom, // 父级容器的dom id为root的 dom  也就是rootfiber
    tag: "host_root", //标识起点
    effects: [], //储存 下级的fiber对象
    child: null, //子集fiber  只有一个子集剩下的都是兄弟节点
    alternate: task.dom.__rootFiberContainer //更新操作, 需要储存alternate 老的fiber,方便后续新旧fiber对比更新
    //render(<div></div>)  render(<span></span>);这种
  }
}



/**
 * 该函数用来构建当前fiber的所有子fiber对象
 * @param {*} fiber 当前的fiber对象
 * @param {*} children  要构建的子级fiber 对象集合
 */
const reconcileChildren = (fiber, children) => {
  /**
   * children 可能对象 也可能是数组
   * 将children 转换成数组
   * 
   * 首次加载调用时 是个对象,后续生成子任务是数组,做个统一数据格式处理
   */

  const arrifiedChildren = arrified(children) //转换为数组格式

  /**
   * 循环 children 使用的索引
   */
  let index = 0
  /**
   * children 数组中元素的个数
   */
  let numberOfElements = arrifiedChildren.length
  /**
   * 循环过程中的循环项 就是子节点的 virtualDOM 对象
   */
  let element = null
  /**
   * 子级 fiber 对象
   */
  let newFiber = null
  /**
   * 上一个兄弟 fiber 对象
   */
  let prevFiber = null

  let alternate = null //旧的fiber对象

  if (fiber.alternate && fiber.alternate.child) {
    //首次渲染是不存在 alternate 的,然后在 commit阶段完成后alternate会存在 
    //也就是说 更新阶段会存在,在这里获取一下子级,因为下面也是 先对子级作对比
    alternate = fiber.alternate.child
  }

  while (index < numberOfElements || alternate) { //同级比对
    //在这里循环要加一个alternate为true的条件,因为我们的fiber是一一对应的,在这里是为了防止element不存在而alternate存在
    //后续为了标识删除操作
    //这个循环判断相当于 一个双层比对,新旧fiber子级互相筛选比对, 存在相同的就更新,新的fiber在旧的中不存在就生成一个新增的fiber
    //新的不存在  旧的存在 就把旧的标记删除
    /**
     * 子级 virtualDOM 对象
     */
    element = arrifiedChildren[index]

    if (!element && alternate) {
      /**
       * 删除操作
       */
      alternate.effectTag = "delete" //删除不用生成fiber, 直接给旧的fiber标识删除
      //然后把它添加到当前fiber ,也就是新生成的effects中,因为我们最后是全部收集到最外层的effects中做的循环处理
      fiber.effects.push(alternate)
    } else if(element && alternate) { // 两个都存在,一一对应,要做更新操作
      /**
       * 更新
       */
      newFiber = {
        type: element.type,
        props: element.props,
        tag: getTag(element),
        effects: [],
        effectTag: "update", //和初始渲染 不一样的是 操作类型不一样。这里标识是更新
        parent: fiber,
        alternate, //因为fiber是一一对应的,我们前面是只对应了最外面root那一层,所以这里也要对应上
        //这里是为了 子任务executeTask 再次进入 reconcileChildren 时 获取child时 获取到alternate(上面代码155行)
      }
      if (element.type === alternate.type) {
        /**
         * 类型相同,不需要生成dom,直接用旧的赋值
         */
        newFiber.stateNode = alternate.stateNode
      } else {
        /**
         * 类型不同,需要重新生成替换
         */
        newFiber.stateNode = createStateNode(newFiber)
      }
    } else if (element && !alternate) {
      //初始渲染
      //子集fiber
      newFiber = {
        type: element.type,
        props: element.props,
        tag: getTag(element),
        effects: [],
        effectTag: "placement",
        parent: fiber
      }
      //为fiber节点添加dom对象或组件实例
      newFiber.stateNode = createStateNode(newFiber)
    }


    //当前fiber的子级 newFiber构建完毕了,当前fiber只有一个子级newFiber, 其他和和这个newFiber平级的都是它的兄弟

    if (index === 0) { //第一个,认为是fiber的子级

      fiber.child = newFiber;
    } else if (element) { //循环首次不会走这里,第1之后的节点,首次循环之后我们会每次把上次的newFiber赋值给prevFiber,第一轮之后就有值了
      //我们每次都把当前这个newFiber设置为 上一个fiber的兄弟节点. 没有兄弟节点的可以认为是 当前子级最后一个
      prevFiber.sibling = newFiber
    }


    if (alternate && alternate.sibling) { //本次对比结束了,要进行下一个比对了,我们知道我们下一个都是通过
      //sibling 兄弟来设置的,所以我们来获取它的下一个兄弟
      alternate = alternate.sibling
    } else { //如果没兄弟了,相当于旧的 fiber 走完了
      alternate = null
    }

    // 更新
    prevFiber = newFiber //首次为null,首次结束之后就有值了,
    //每次循环结束后  把当前这个newfiber赋值给prevFiber, 下次循环时他就是上一个fiber
    index++
  }
  //到这里当前的 子级children fiber构建完毕


}







/**
 * 这里的设计是每一轮未被打断的新的更新,最开始的任务 (render(),setState)都是从rootFiber 来生成子任务.
 * 生成一个子任务 返回 当前fiber 的child fiber 或者 它的sibling fiber 兄弟 以及给当前 fiber的
 * effects中收集 子孙级的fiber对象, 并且设置pendingCommit 最外层的fiber对象
 * @param {*} fiber 传入的fiber对象 
 */
const executeTask = fiber => {

  /**
   * 开始构建子fiber对象,对于后面来说每个fiber都是一个子任务  
   */

  if (fiber.tag === 'class_component') { //如果当前传入的fiber是一个类组件类型
    if (fiber.stateNode.__fiber && fiber.stateNode.__fiber.partialState) { //当前fiber是组件类型
      //这个时候如果stateNode如果要是存在的话,那就不是首次渲染的情况了,是更新
      //在我们目前的设计中只有setState会触发更新
      fiber.stateNode.state = { //所以进行新旧state合并
        ...fiber.stateNode.state,
        ...fiber.stateNode.__fiber.partialState
      }
    }
    reconcileChildren(fiber, fiber.stateNode.render()) //通过render函数获取 类组件的子集
  } else if (fiber.tag === "function_component") { //函数直接调用
    reconcileChildren(fiber, fiber.stateNode(fiber.props))
  } else {
    reconcileChildren(fiber, fiber.props.children) //只构建当前这一个下的子fiber
  }

  /**
   * 这里构建完毕当前fiber的 子fiber了
   * 如果子级存在 返回子级
   * 然后将这个子级当做父级 继续构建这个父级下的子级
   */
  console.log(fiber)
  if (fiber.child) { //这里返回了child, 我们会循环调用executeTask,然后下次传入它的参数就是我们返回的child,然后会构建
    //它的子fiber
    return fiber.child
  }
  /**
   * 如果存在同级 返回同级 构建同级的子级
   * 如果同级不存在 返回到父级 看父级是否有同级
   * 
   * 这里是优先构建子级,如果当前fiber没有子级了,再去构建它的同级,同级不存在返回父级
   */
  let currentExecutelyFiber = fiber
  while (currentExecutelyFiber.parent) { //如果有父级,有父级时才有可能有子级
    currentExecutelyFiber.parent.effects = currentExecutelyFiber.parent.effects.concat(
      //把自己当前的fiber 和自己收集到的自己子fiber 的effects 合并放到 父级的effects中
      //这样最后在外面的fiber的effects中就会有所有所属它下的fiber
      currentExecutelyFiber.effects.concat([currentExecutelyFiber])
    )

    if (currentExecutelyFiber.sibling) { //如果当前fiber有同级,返回同级 来构建同级的子集
      return currentExecutelyFiber.sibling
    }

    currentExecutelyFiber = currentExecutelyFiber.parent //没有同级的时候 返回它的父级 ,看看它的父级是否有同级 需要构建
  }

  //走到这里时也就是说找到了最外层的fiber了,rootfiber,因为已经没有父级了
  //这会就表明 我们的一次完整 由rootfiber开始生成子任务的 fiber的过程完毕了
  //这会我们需要一个变量存储最外层的rootfiber,然后标识进入下一commit阶段了

  pendingCommit = currentExecutelyFiber
}






const workLoop = deadline => {
  /**
   * 如果子任务不存在 就去获取子任务
   */
  if (!subTask) {
    subTask = getFirstTask()
  }
  /**
   * 如果任务存在并且浏览器有空余时间就调用
   * executeTask 方法执行任务 接受任务 返回新的子任务, 它会生成fiber对象,它是可以被打断的
   */
  while (subTask && deadline.timeRemaining() > 1) {
    subTask = executeTask(subTask)
  }

  if (pendingCommit) {
    commitAllWork(pendingCommit)
  }
}





/**
 * 在事件循环空闲时即将被调用的函数,也就是说它是浏览器有空余时间时要执行的函数
 * @param {*} deadline 这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态
 */
const performTask = deadline => {
  /**
   * 浏览器空闲了,执行任务
   */
  workLoop(deadline)
  /**
   * 判断任务是否存在
   * 判断任务队列中是否还有任务没有执行
   * 再一次告诉浏览器在空闲的时间执行任务
   */
  if (subTask || !taskQueue.isEmpty()) { //一个任务 会生成 子集fiber 返回子任务 这个过程是优先级较低的任务是可以被打断的
    //这里判断如果被打断了   而且子任务还没有执行完毕,要继续调用requestIdleCallback 等待有空余时间继续执行
    requestIdleCallback(performTask)
  }
}





export const render = (element, dom) => {
  /**
   * 1. 向任务队列中添加任务
   * 2. 指定在浏览器空闲时执行任务
   */
  /**
   * 任务就是通过 vdom 对象 构建 fiber 对象
   */
  taskQueue.push({
    dom,
    props: { children: element }
  })
  /**
   * 指定在浏览器空闲的时间去执行任务,空闲时执行performTask函数
   */
  requestIdleCallback(performTask)
}

//组件更新的方法
export const scheduleUpdate = (instance, partialState) => { //组件更新时的方法
  taskQueue.push({    //往队列里push一个 组件类型任务
    from: "class_component",
    instance, //组件实例
    partialState //新的状态
  })
  requestIdleCallback(performTask)
}

其实所有的核心都在这个协调算法里,其他文件里的都是一些辅助函数.
开始位置有git地址,有兴趣的小伙伴可以下载下来跑跑看.
里面的代码都有jsdoc的注释.

在这里说明一下,这里代码只是做的编码思想上的一个实现,主要的核心还是任务可中断和任务优先级,其他的例如setState的合并调度fiber更新流程,以及归的时候的更新节点收集这些问题是没有重点去写的,这里只是简单了解一下fiber架构的核心思想.

后续会出一篇,react源码分析的文.

阅读 390

世界核平

22 声望
11 粉丝
0 条评论
你知道吗?

世界核平

22 声望
11 粉丝
宣传栏