从二到三实现统一提交

124 阅读2分钟

mini-react 第一篇:从零到一实现最最最基本的 mini-react

mini-react 第二篇:从一到二实现 fiber 架构

一、什么是统一提交?

当初次接触到新的名词时,总是烦躁怎么会造出这么多花里胡哨的东西?等用惯了,多好的名字哇,统一的定义,无需再赘言原理。(当然不包括万恶的黑话)

统一提交是指,在完成 dom 的创建后,将挂载的动作统一放到最后来执行,一次性执行完毕。

但为什么有这种方案的出现呢?

思考这样一个场景:由于某个 dom 的挂载带着非常炫酷的动画,或者在某个 dom 挂载后,js 引擎又被插入了各种任务,于是 js 引擎繁忙中,requestIdleCallback 一直不被调用,完整的页面就只展示到了这个 dom 就此暂停,等待许久才可能进入下一个 dom 的挂载。用户:摇头.jpg

统一提交就可以有效解决这个问题。反正都是等,白屏总比只显示一半好(对于白屏,用户可能以为是网络不好,可只显示一半锅就全全丢到开发身上了)。

二、实现思路

  1. 将挂载 dom 的内容抽取到一个新的函数 commitWork(fiber) 中 => 这是挂载单个 dom 节点的函数
  2. 挂载所有节点可通过 commitWork 遍历 fiber 架构实现 => 递归遍历 child 与 sibling
  3. 由于是统一提交,一次性完成所有 dom 的挂载,而非等待浏览器空闲时期挂载,所以需要一个总的入口函数 commitRoot,里面使用 commitWork
  4. 统一提交的时机为所有虚拟节点对应的 dom 都创建完毕,由上一篇从一到二实现 fiber 架构可知,是 nextUnitOfWork 的值为 undefined 的时候

三、代码实现

/**
 * 普通组件
 * @param {Object} vnode
 */
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    // 1. 创建 DOM 节点
    const dom = fiber.dom =
      fiber.type === "ELEMENT_TEXT"
        ? document.createTextNode("")
        : document.createElement(fiber.type);

    // 2. 赋值 props
    for (const key in fiber.props) {
      if (key === "children") continue;
      dom[key] = fiber.props[key];
    }
  }

  // 3. 处理 props.children
  initChildren(fiber, fiber.props.children)

  /** 移至 commitWork 函数内
  // 4. 挂载
  let parentFiber = vnode.parent;
  if (parentFiber) {
    while (!parentFiber.dom) {
      parentFiber = parentFiber.parent;
    }
    parentFiber.dom.appendChild(vnode.dom);
  } 
  */
}

function workLoop(deadline) {
  let shouldYield = false
  // nextUnitOfWork 不为空的判断补上,否则总会再次调用 performUnitOfWork
  if (!shouldYield && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }

  // 统一提交的时机
  if (!nextUnitOfWork && root) {
    commitRoot()
  }

  requestIdleCallback(workLoop)
}

/**
 * 提交入口
 */
function commitRoot() {
  commitWork(root.child)
  // 提交完毕需清空 root,否则重复提交
  root = null
}

/**
 * 一次挂载
 */
function commitWork(fiber) {
  if (!fiber) return;

  let parentFiber = fiber.parent;
  if (parentFiber) {
    while (!parentFiber.dom) {
      parentFiber = parentFiber.parent;
    }
    fiber.dom && parentFiber.dom.appendChild(fiber.dom);
  }

  // 遍历
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}