Build your own React(3)-模拟fiber架构工作模式

204 阅读5分钟

原文链接:Build your own React

  • Step I: The  createElement Function ✅
  • Step II: The  render Function ✅
  • Step III: Concurrent Mode 并发模式 ✅
  • Step IV: Fibers ✅
  • Step V: Render and Commit Phases
  • Step VI: Reconciliation
  • Step VII: Function Components
  • Step VIII: Hooks

** To avoid confusion, I’ll use “element” to refer to React elements and “node” for DOM elements.*

前言

上文中我们完成了基本的createElement以及render函数,但是还遗留了一些问题,这篇文章将会给出解决方案。

Concurrent Mode(并发模式)

在我们继续添加功能之前,我们需要对已有的代码进行下重构。问题出在render函数中递归渲染的方式,一旦我们开始rendering,直到整棵树渲染成功后才会停止,如果DOM树比较大,可能会阻塞主线程太久。如果浏览器需要处理高优先级的事情,比如处理用户输入或者保持动画流畅,那么主线程必须要等到渲染完成才可以处理其他事件。

所以我们要把工作分解成一个个的小部分,在完成每个小部分后,如果有其他事情不得不处理,我们会让浏览器中断渲染。这里我们主要会使用requestIdleCallback来重构我们的代码。关于此api,可以在我的另一篇文章查看(强烈建议先阅读下,否则理解下面的代码会有些费力)。

重构后需添加如下代码:

// react/render.js// 待处理的任务
let nextUnitOfWork = null;
​
function workLoop(deadline) {
  // 剩余时间是否足够执行任务的标识
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    // performUnitOfWork处理当前任务并返回下一个待执行的任务
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  // 本次没有足够空闲时间 请求下一次浏览器空闲的时候执行
  requestIdleCallback(workLoop);
}
// 初始化执行workLoop
requestIdleCallback(workLoop);
​
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}
​

接下来我们需要初始化nextUnitOfWork,并且补充performUnitOfWork函数的逻辑,此函数不仅要负责处理当前任务,还需要返回下一个待处理的任务,如果有的话。

Fiber

终于迎来了React中一个十分重要的概念,如果你已经阅读了上面提及的文章初识Fiber以及RIC,那么此节对你来说很轻松就可以理解。

我们将会为每一个元素创建其对应的fiber,每一个fiber就是一个小的工作单元,举例说明:

如果我们想渲染如下DOM:

render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

首先需要在render函数中初始化nextUnitOfwork,剩余的工作将发生在上节提到的performUnitOfWork函数中,此函数会对每个fiber做三件事:

  • 添加元素至DOM中
  • 为每个子元素创建fiber
  • 选择下一个工作单元并返回

逐步构建出如下的Fiber树:

Fiber.png

show code

首先将render函数中的代码分离至createDOM函数中:

// react/render.js
function createDOM(fiber) {
  // 创建元素
  const dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
​
  // 赋予属性
  Object.keys(fiber.props)
    .filter((key) => key !== "children")
    .forEach((name) => (dom[name] = fiber.props[name]));
​
  return dom;
}
​
function render(element, container) {
  // 初始化下一个工作单元(根节点)
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
    sibling: null,
    child: null,
    parent: null,
  };
}
​

当浏览器就绪时,将会调用我们的workLoop函数并在初始化的工作单元(根节点)上工作。

接下来补充performUnitOfWork函数:

function performUnitOfWork(fiber) {
  // debugger
  // 生成DOM
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
​
  // 如果当前节点存在父节点,将其挂载到父节点下
  if (fiber.parent) {
    fiber.parent.dom.append(fiber.dom);
  }
​
  const elements = fiber.props.children;
  // 记录上一个fiber
  let prevSibling = null;
​
  // 为每个子节点创建fiber
  for (let i = 0; i < elements.length; i++) {
    const newFiber = {
      // 记录元素标签类型
      type: elements[i].type,
      // 记录元素属性
      props: elements[i].props,
      // 与父节点创建链接
      parent: fiber,
      // 此时DOM还未生成 初始化null
      // 下一次进入performUnitOfWork时
      // 通过createDOM函数生成
      dom: null,
      // 与第一个子节点关联 初始化null
      child: null,
      // 与兄弟节点创建链接 初始化null
      sibling: null,
    };
​
    if (i === 0) {
      // 与第一个子节点成功关联
      fiber.child = newFiber;
    } else {
      /**
       * 根据fiber的架构设计,只有第一个子节点关联至child属性
       * 其他子节点将关联到第一个子节点的兄弟节点 通过fiber.child.sibling访问到
       * 此处的prevSibling实际上就是上一个子节点生成的fiber
       * 这段代码也可以理解为 fiber.child.sibling = newFiber
       * (此处有些不易理解,涉及到js引用类型的浅拷贝,大家可以多想一想)
       */ 
      prevSibling.sibling = newFiber;
    }
    // 保存当前fiber
    prevSibling = newFiber;
  }
​
  // 按照Fiber架构渲染顺序
  // 有子节点 优先处理子节点
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    // 没有子节点的时候 优先处理兄弟节点
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    // 当此层的兄弟节点也处理完时
    // 返回其父节点 继续处理其父节点所在层的兄弟节点 
    nextFiber = nextFiber.parent;
  }
}

tips:第一次看这段代码可能会有些难以理解,建议大家一定要动手debugger下(在performUnitOfWork函数第一行debugger),单步调试观察下每次是如何处理fiber,如何生成,如何渲染,如何返回下一次工作单元。

总结

每篇文章都不会过长,从这篇文章开始会没那么容易理解,希望大家可以亲身动手尝试下。当然目前这段代码设计还是存在问题的,我们每次处理一个元素时,都会向DOM添加一个新节点,但是记住,浏览器可能会在我们完成渲染整个树之前中断我们的工作。在这种情况下,用户将看到一个不完整的 UI。这个问题将在下个文章解决。