从一到二实现 fiber 架构

148 阅读3分钟

为什么是“从一到二”?因为“一”在上一篇,本篇的内容是在“一”的基础上实现的。

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

mini-react 第三篇: 从二到三实现统一提交

一、为什么使用 fiber 架构?

从已知者的角度来讲,fiber 架构其实是对 DOM 挂载的优化。那么它又是针对什么的优化呢?

请考虑这样一个场景:当我们有非常大的一棵虚拟 DOM 树需要创建标签并挂载时,js 引擎一直在工作,页面会丢失交互的响应。——这里补充一个小知识,js 引擎是单线程工作的,js 文件、鼠标点击事件、页面滚动事件等都需要由 js 引擎来执行,这些动作需一个执行完毕后才能进入下一个。

由于 js 引擎执行完任务会进入空闲阶段,我们正好可以利用这个空闲阶段来执行上述场景中的动作 —— 创建并挂载 DOM 节点。

这样一来,就需要一个算法,使得我们能够边遍历到树的每一个虚拟 dom 节点。—— 像我这种笨蛋第一时间想到的是数组(开始浪费空间.jpg。

而聪明的工程师想到了 fiber 架构 —— 将一棵不规则的树转化成一棵 child-sibling 二叉树(当一棵树的每个节点,子节点是它的 child 和 sibling,这不正是一棵二叉树吗!),于是只需记录根节点。

二、利用空闲阶段

—— 任务调度器

浏览器为我们提供了一个空闲时期回调的 API —— requestIdleCallback

function workLoop(deadline) {
  let shouldYield = false;
  if (!shouldYield) {
    console.log(111);
    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

三、Fiber 架构

3.1 建立 child-sibling 树

/**
 * 普通组件
 * @param {Object} vnode
 */
function updateHostComponent(vnode) {
  // ... 创建 DOM 节点,处理 props(除children)

  // 3. 处理 props.children
  let prevChild = null;
  vnode.props.children.forEach((child, index) => {
    if (index === 0) {
      vnode.child = child;
    } else {
      prevChild.sibling = child;
    }
    child.parent = vnode;
    prevChild = child;
  });

  // ... 挂载 DOM
}

/**
 * 函数组件
 * @param {Object} vnode
 */
function updateFunctionComponent(vnode) {
  const child = vnode.type();

  vnode.child = child;
  child.parent = vnode;
}

3.2 遍历 fiber 架构

简而言之,是先序遍历。

// 1. return child
if (fiber.child) return fiber.child;
// 2. return sibling
if (fiber.sibling) return fiber.sibling;
// 3. return 叔叔/祖叔叔/太叔叔...
let fiberParent = fiber.parent;
while (fiberParent && !fiberParent.sibling) fiberParent = fiberParent.parent;
return fiberParent?.sibling;

3.3 完整代码

/** React.js */
// 记录当前根节点
let root = null;
// 记录下一个空闲时期要执行的任务
let nextUnitOfWork = null;

function render(vnode, container) {
  root = {
    dom: container,
    props: {
      children: [vnode]
    }
  };
  nextUnitOfWork = root;
}

/**
 * 函数组件
 * @param {Object} vnode
 */
function updateFunctionComponent(vnode) {
  const child = vnode.type();

  vnode.child = child;
  child.parent = vnode;
}

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

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

  // 3. 处理 props.children
  let prevChild = null;
  vnode.props.children.forEach((child, index) => {
    if (index === 0) {
      vnode.child = child;
    } else {
      prevChild.sibling = child;
    }
    child.parent = vnode;
    prevChild = child;
  });

  // 4. 挂载
  let parentFiber = vnode.parent;
  if (parentFiber) {
    while (!parentFiber.dom) {
      parentFiber = parentFiber.parent;
    }
    parentFiber.dom.appendChild(vnode.dom);
  }
}

/**
 * 一次只处理一个任务,并返回下一个任务
 * @param {object} fiber
 */
function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }

  /* 先序遍历 */
  // 1. return child
  if (fiber.child) return fiber.child;
  // 2. return sibling
  if (fiber.sibling) return fiber.sibling;
  // 3. return 叔叔/祖叔叔/太叔叔...
  let fiberParent = fiber.parent;
  while (fiberParent && !fiberParent.sibling) fiberParent = fiberParent.parent;
  return fiberParent?.sibling;
}

function workLoop(deadline) {
  let shouldYield = false;
  // nextUnitOfWork 不存在时,恭喜,完成所有 dom 创建与挂载任务。
  if (!shouldYield && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

为验证该代码是否正确,App 的代码修改如下

import React from './core/React';

const App = function () {
  return (
    <div id="app">
      <div>
        <h2>
          歌手:<span>容祖儿</span>
        </h2>
      </div>
      <ul>
        <li>东京人寿</li>
        <li>再见我的初恋</li>
        <li>隆重登场</li>
        <li>华丽邂逅</li>
      </ul>
    </div>
  );
};

export default App;

3.4 代码抽取与改进

  1. 处理 children 的步骤一致,可抽取。
  2. props.children 的处理中,修改了原有 child,不太合适,改为复制一个新的对象作为 child 使用。
/**
 * 函数组件
 * @param {Object} vnode
 */
function updateFunctionComponent(vnode) {
  const child = vnode.type();

  initChildren(vnode, [child]);
}

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

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

  // 3. 处理 props.children
  initChildren(vnode, vnode.props.children);

  // 4. 挂载
  let parentFiber = vnode.parent;
  if (parentFiber) {
    while (!parentFiber.dom) {
      parentFiber = parentFiber.parent;
    }
    parentFiber.dom.appendChild(vnode.dom);
  }
}

function initChildren(fiber, children) {
  let prevChild = null;
  children.forEach((child, index) => {
    // 用新的对象,不改变原有对象
    let newChild = {
      ...child,
      dom: null,
      child: null,
      sibling: null,
      parent: null
    };
    if (index === 0) {
      fiber.child = newChild;
    } else {
      prevChild.sibling = newChild;
    }
    newChild.parent = fiber;
    prevChild = newChild;
  });
}

3.5 重命名

自行将 vnode 改为 fiber —— 毕竟是 fiber 架构[手动狗头]