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

本文完,感谢阅读 🌹

相关推荐
可乐鸡翅yeah_5 分钟前
hls.js 播放质量埋点实战,采集卡顿、起播、错误指标定位线上用户问题
开发语言·前端·javascript·ecmascript·音视频·m3u8·音视频在线播放
tedcloud12325 分钟前
Wand-Enhancer:如何搭建一套远程开发与测试环境
前端·人工智能·macos·开源·流程图
掘金挖土1 小时前
前端手摸手跑路之 AI 应用开发(三)
前端·后端
计算机魔术师1 小时前
OpenAI 宣称破解千禧年难题,数学家却说是「反例搜索」?
前端
志尊宝1 小时前
Vue3 零基础每日笔记(015):条件渲染 v-if 与 v-show——控制 DOM 生死还是控制显隐
前端·vue.js·笔记
掘金者阿豪1 小时前
OceanBase 和金仓怎么选?别只看分布式,复杂查询更考验架构取舍
前端·后端·架构
Bug修理工Bubble1 小时前
Swift ARC:一个对象到底什么时候才会被释放?
前端
ClouGence1 小时前
GPT-6 做 UI 自动化测试:Demo 惊艳,但真的适合长期回归吗?
前端·chatgpt·测试
阿酷tony2 小时前
视频专栏列表的防录屏水印和跑马灯效果(也可网站调用)
java·前端·音视频
Bs_MoneyMagnet2 小时前
基于springboot+vue的宠物殡葬管理平台的设计与实现 源码+文档
java·javascript·vue.js·spring boot·后端·宠物