手写mini-React(四)Fibers

为了组织工作单元,我们需要一个数据结构:fiber树。

每个元素都是一个 fiber,每个 fiber 都是一个工作单元。

让我们来看一个例子:

假设我们想要渲染一个这样的元素树

js 复制代码
Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

这张图下面会用到。

在渲染过程中,我们将创建一个 root fiber 对象,并将其赋值给 nextUnitOfWork。

剩下的工作将发生在 performUnitOfWork 函数上,我们将为每个 fiber 做三件事:

  1. 将元素添加到 DOM
  2. 为当前元素的每个孩子创建 fiber
  3. 选择下一个工作单元

这种结构的目标之一是便于查找下一个工作单元。这也是为什么每个 fiber 都有一个连接到它第一个 child、下一个 sibling 和它的 parent 的链接。(当然,没有的话链接指向 null)

child -> 孩子,sibling -> 兄弟姐妹, parent -> 父母。在这里不翻译这几个单词。因为在 fiber 的结构中有这三个 key。

当我们在一个 fiber 上完成了需要执行的工作,如果它还有一个 child,那么这个 child 就是下一个工作单元。在上面 Fiber Tree 的图中,当我们完成了对 div fiber 的处理时,下一个工作单元就成了 h1。

但如果当前 fiber 没有 child,我们会将它的 sibling 作为下一个工作单元。例如,p fiber 没有 child,因此我们将 a fiber 作为下一个工作单元。

如果这个 fiber 既没有 child,也没有 sibling,我们将会去寻找它的 uncle,例如图中的 a 和 h2。

如果这个 parent 没有 sibling,那我们就通过 parent 一直往上找,直到找到一个 sibling,或是找到最终的 root 。如果我们已经到达了 root,这意味着我们已经完成了这个渲染的所有工作。

整个过程其实就是进行深度优先遍历。

这也和我们古代的君位传承的方式一致,一是父死子继,二是兄终弟及。

现在让我们写一下代码。

首先让我们先从 render 函数里移除代码。

js 复制代码
function createDOM(fiber) {
  const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  // 将属性分配至生成的 DOM 节点
  const isProperty = (key) => key !== "children";
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((key) => {
      dom[key] = element.props[key];
    });

  return dom;
}

function render(element, container) {
  // TODO set next unit of work
}

let nextUnitOfWork = null;

我们将创建 DOM 节点的部分保留在函数里,我们之后会使用它。

js 复制代码
function render(element, container) {
  // TODO set next unit of work
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  };
}

let nextUnitOfWork = null;

在 render 函数里,我们将 nextUnitOfWork 设为 fiber 树的根。

然后,当浏览器准备好的时候,它将调用我们的 workLoop,我们将开始处理 root。

js 复制代码
let nextUnitOfWork = null;

function workLoop(deadline) {
  let shouldYield = false;

  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

function performUnitOfWork(fiber) {
  // TODO add dom node
  // TODO create new fibers
  // TODO return next unit of work
}

首先,我们先判断传入的工作单元(其实就是 createElement 函数返回的对象)是否有创建好的 DOM 节点,没有的话为它创建 DOM 节点(根 fiber 是有 DOM节点的,就是 render 函数传入的 container )。

然后判断当前 fiber 是否有 parent,有的话就将当前 fiber 中的 dom(刚才创建的),追加到( append )它父节点的 dom 中。(这一步就是在一步步地构建 dom 节点树)

js 复制代码
function performUnitOfWork(fiber) {
  // TODO add dom node
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // TODO create new fibers
  // TODO return next unit of work
}

然后,对于每一个子元素(其实就是 createElement 函数返回的对象),我们都会创建一个新的 fiber。

具体的实现方式就是遍历当前 fiber 的子元素列表。

js 复制代码
function performUnitOfWork(fiber) {
  // TODO add dom node
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // TODO create new fibers
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };
  }
  // TODO return next unit of work
}

此外,我们还需要将它添加到 fiber 树中,根据它是否是第一个孩子,将它设置为 child 或是 sibling。

在遍历的过程中,如果被遍历到的元素是第一个孩子,就将它设为传入 performUnitOfWork 的 fiber 的 child,如果不是,就将它设为长子的 sibling,并且还要将这个它存到提前声明的 prevSibling 中,这一步主要是为了设置 sibling。最后形成这样的 sibling 链:长子(数组第一个元素) -> 次子 -> ... -> 最小的孩子(数组最后一个元素)。

js 复制代码
function performUnitOfWork(fiber) {
  // TODO add dom node
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // TODO create new fibers
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
  // TODO return next unit of work
}

那么最后一步就是返回下一个工作单元了。

按照我们之前所讲的,下一个工作单元的第一继承人就是嫡长子(第一个子元素)。如果没有孩子,那么就只能按照兄终弟及,让自己的弟弟成为下一个工作单元。如果既没有孩子也没有兄弟,那就只能让父亲的兄弟来做下一个工作单元了。

当 nextFiber 为空,也就是说 parent 指向为空的时候,树的回溯已经回溯到树根(root)了,也就表明此时整棵树的处理工作已经结束了。

js 复制代码
function performUnitOfWork(fiber) {
  // TODO add dom node
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // TODO create new fibers
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
  // TODO return next unit of work
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

完整代码

js 复制代码
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      ),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

function createDOM(fiber) {
  const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  // 将属性分配至生成的 DOM 节点
  const isProperty = (key) => key !== "children";
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((key) => {
      dom[key] = element.props[key];
    });

  return dom;
}

function render(element, container) {
  // TODO set next unit of work
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  };
}

let nextUnitOfWork = null;

function workLoop(deadline) {
  let shouldYield = false;

  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

function performUnitOfWork(fiber) {
  // TODO add dom node
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // TODO create new fibers
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index];

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
  // TODO return next unit of work
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

const Didact = {
  createElement,
  render,
};

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
);
const container = document.getElementById("root");
Didact.render(element, container);

简单总结下,fiber 就是一个数据结构、一个 JavaScript 对象,它是由 createElement 函数的返回值扩充过来的。这个扩充的过程就是 fiber 树形成的过程,也是 dom 树形成的过程。在这个过程中,是以深度优先搜索的方式来寻找下一个工作单元。

相关推荐
哑巴语天雨13 小时前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情13 小时前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起14 小时前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱14 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
高山我梦口香糖17 小时前
[react] <NavLink>自带激活属性
前端·javascript·react.js
撸码到无法自拔17 小时前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖17 小时前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js
乐闻x20 小时前
VSCode 插件开发实战(四):使用 React 实现自定义页面
ide·vscode·react.js
irisMoon0620 小时前
react项目框架了解
前端·javascript·react.js
web150850966411 天前
【React&前端】大屏适配解决方案&从框架结构到实现(超详细)(附代码)
前端·react.js·前端框架