创建自己的 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

本文完,感谢阅读 🌹

相关推荐
迷雾漫步者30 分钟前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试