创建自己的 React (中)

前言

接上文 创建自己的 React(上),目前已经实现把 JSX 渲染到页面,但是因为递归调用了 render 方法,如果元素树很大,页面渲染的耗时会很久。

渲染优化

因此需要将工作分解成数份,在每份工作完成后,如果浏览器需要做其他高优先级的事情,就终止渲染任务。

添加 nextUnitOfWork 表示下一个工作任务,使用 workLoop 函数用来执行工作循环,performUnitOfWork 函数用来实现每个工作任务要做的事情。

ts 复制代码
/// hypereact.ts

export function render(element, container) {
  /// ...
}

let nextUnitOfWork = null;

function workLoop(deadline: IdleDeadline) {
  let shouldYield = false;

  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  requestIdleCallback(workLoop);
}

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

添加 Fiber 树

为了分解工作,使用 Fiber 树这种数据结构,每个树上的元素表示一份任务,同时元素包含指向它的子元素,父元素,相邻元素的指针。

整个 Fiber 树的工作顺序是从顶到下,再从底到上。如果一个元素有子元素,那么下一个工作元素就是这个子元素,如果没有子元素有相邻元素,那么下一个工作元素就是这个相邻元素,如果都没有那就去找父元素的相邻元素,最终到达根元素 root

js 复制代码
/// hypereact.ts

function createDom(fiber) {
  const dom: HTMLElement =
    fiber.type == TEXT_ELEMENT
      ? document.createTextNode('')
      : document.createElement(fiber.type);
  const isProperty = (key) => key !== 'children';

  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((name) => {
      if (dom.setAttribute) {
        dom.setAttribute(name, fiber.props[name]);
      } else {
        dom[name] = fiber.props[name];
      }
    });

  return dom;
}

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

let nextUnitOfWork = null

每个 Fiber 具体的任务在 performUnitOfWork 函数中实现,要做的工作是

  1. 为 Fiber 创建 DOM 节点
  2. 为 Fiber 的子元素创建 DOM 节点
  3. 找到下一个 Fiber 作为任务
ts 复制代码
/// hypereact.ts

function performUnitOfWork(fiber) {
  // 为 Fiber 创建 DOM 节点
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }

  // 为 Fiber 的子元素创建 DOM 节点
  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++;
  }

  // 找到下一个 Fiber 作为任务
  if (fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;

  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

渲染和提交

现在已经能够使用 Fiber 树进行渲染了,但是问题是如果浏览器中断了渲染任务,那么浏览器上会得到一个不完整的 UI 界面。

使用一个 wipRoot 追踪 Fiber 树的根,当整个 Fiber 树的任务都完成后,就将根节点添加到文档中去。

ts 复制代码
/// hypereact.ts

function commitRoot() {
  commitWork(wipRoot.child);
  wipRoot = null;
}

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  const domParent = fiber.parent.dom;
  domParent.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

export function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  };
  nextUnitOfWork = wipRoot;
}

let nextUnitOfWork = null;
let wipRoot = null;

function performUnitOfWork(fiber) {
  /// ...

  // if (fiber.parent) {
  //  fiber.parent.dom.appendChild(fiber.dom)
  // }

  /// ...
}

参考

本文代码

创建自己的 React(上)

Build your own React

本文完,感谢阅读 🌹

相关推荐
不听话坏2 小时前
Ignition篇(下 一) 动态执行前的事情
开发语言·前端·javascript
likeyi072 小时前
require 和 import的区别
开发语言·前端
pany3 小时前
做 AI 友好的开源 Vue3 模板 🌈
前端·vue.js·ai编程
小二·3 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·react.js
谙忆10244 小时前
前端图片直传对象存储:OSS/S3 预签名 URL、STS 临时凭证与回调校验
前端
CHNE_TAO_EMSM5 小时前
Android studio 打开文件时自动下载源码
前端·javascript·android studio
一孤程6 小时前
Airtest自动化测试第五篇:小程序与Web测试——跨平台自动化全覆盖
前端·自动化测试·小程序·自动化·测试·airtest
IT_陈寒6 小时前
SpringBoot自动配置不是你以为的那样的智能
前端·人工智能·后端
yume_sibai7 小时前
大屏数据可视化 - 边框红绿呼吸灯实现详解
前端·信息可视化·typescript
竹林8187 小时前
从 ethers.js 迁移到 Viem:一个签名验证 Bug 让我彻底放弃旧爱
javascript