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

本文完,感谢阅读 🌹

相关推荐
Bellafu6661 小时前
selenium 常用xpath写法
前端·selenium·测试工具
blackorbird4 小时前
Edge 浏览器 IE 模式成攻击突破口:黑客借仿冒网站诱导攻击
前端·edge
谷歌开发者4 小时前
Web 开发指向标 | Chrome 开发者工具学习资源 (一)
前端·chrome·学习
名字越长技术越强5 小时前
Chrome和IE获取本机ip地址
前端
天***88965 小时前
Chrome 安装失败且提示“无可用的更新” 或 “与服务器的连接意外终止”,Chrome 离线版下载安装教程
前端·chrome
半梦半醒*5 小时前
zabbix安装
linux·运维·前端·网络·zabbix
大怪v5 小时前
【搞发🌸活】不信书上那套理论!亲测Javascript能卡浏览器Reader一辈子~
javascript·html·浏览器
清羽_ls5 小时前
React Hooks 核心规则&自定义 Hooks
前端·react.js·hooks
你的人类朋友5 小时前
“签名”这个概念是非对称加密独有的吗?
前端·后端·安全
西陵5 小时前
Nx带来极致的前端开发体验——任务缓存
前端·javascript·架构