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

本文完,感谢阅读 🌹

相关推荐
前端拾光者37 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
Json_181790144801 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网1 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子02041 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing1 小时前
React核心功能详解(一)
前端·react.js·前端框架
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
深度混淆2 小时前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China2 小时前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q2 小时前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge
滴水可藏海2 小时前
Chrome离线安装包下载
前端·chrome