mini-react(二)实现任务调度器&fiber架构

上节我们使用递归的方式完成了将虚拟dom树转换为真实节点,但如果dom树的子节点过多时,浏览器就会出现卡顿。

原因是JS是单线程的,我们执行的任务过多,阻塞了渲染引擎,导致出现卡顿。

所以我们需要对任务进行拆分,JS 执行一部分,然后渲染引擎渲染一部分,完成之后,JS 再继续执行,渲染引擎再渲染。

浏览器给我们提供了一个钩子函数 requestIdleCallback 在空余时间执行我们想要的逻辑

requestIdleCallback

requestIdleCallback(callback[, options])

callback 是需要执行的任务,接收一个 IdleDeadline 对象作为参数。IdleDeadline 包含 2 个重要字段

  1. didTimeout,布尔值,表示任务是否超时
  2. timeRemaining() ,用于获取当前帧的剩余时间

options 是一个可选参数,目前只有一个值 timeout,表示如果超过这个时间,任务还没有执行,则强制执行任务,不需要等待空闲时间。

实现任务调度器

我们将大任务拆为多个task,再利用 requestIdleCallback 在浏览器空闲时去执行每个task,这样就可以实现边执行JS边渲染的效果了

我们定义一个变量 shouldYield 来控制是否去执行task,在空闲时间不足时跳过任务的执行

js 复制代码
let taskId = 0;
function workLoop(deadline) {
  taskId++;
  let shouldYield = false;
  while (!shouldYield) {
    if (deadline.timeRemaining() < 0) {
      shouldYield = true;
    }
    //模拟task的执行
    console.log(`run task${taskId}`);
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

实现fiber架构

首先我们定义一个变量代表我们的task:let nextWorkOfUnit = null;

然后定义执行任务的函数,参数为task,它返回下一个需要执行的task:

function performWorkOfUnit(fiber) {}

这样便可以使用任务调度器去代替之前的递归操作了:

js 复制代码
function workLoop(deadline) {
  let shouldYield = false;
  while (!shouldYield && nextWorkOfUnit) {
    shouldYield = deadline.timeRemaining() < 1;
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit);
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

接下来我们去实现 performWorkOfUnit:

首先肯定是要执行上节的render中的逻辑,将构建的 vdom 转换为真实dom,即创建dom和处理props,我们将这两个步骤抽离出来:

js 复制代码
function createDom(type) {
  return type === "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(type);
}

function updateProps(dom, props) {
  Object.keys(props).forEach((prop) => {
    if (prop !== "children") {
      dom[prop] = props[prop];
    }
  });
}

performWorkOfUnit需要返回下一个执行的task,那我们该如何去知道下一个任务该执行什么呢?这里便需要我们将dom树去转换为链表,再通过链表去寻找下一个任务

定义链表的规则为:

  1. 子节点child
  2. 兄弟节点sibling
  3. 叔叔节点parent.sibling

a --> b --> d --> e --> c --> f --> g --> 结束

我们也将这一步抽离为单独的函数:

js 复制代码
function initChildren(fiber) {
  const children = fiber.props.children;
  let prevChild = null;
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      child: null,
      parent: fiber,
      sibling: null,
      dom: null,
    };
    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevChild.sibling = newFiber;
    }
    prevChild = child;
  });
}

为了不破坏虚拟dom的结构,我们定义了一个新的对象newFiber,其dom属性为append的位置,同时定义变量prevChild来保存上一个子节点的内容,用来设置sibling的指向

js 复制代码
function performWorkOfUnit(fiber) {
  if (!fiber.dom) {
    //创建dom,设置添加的位置
    const dom = (fiber.dom = createDom(fiber.type));
    //添加dom
    fiber.parent.dom.append(dom);
    //处理props
    updateProps(dom, fiber.props);
  }

  //转换链表
  initChildren(fiber);

  //返回下一个任务 :1.child 2.sibling 3.parent.sibling
  return fiber.child
    ? fiber.child
    : fiber.sibling
    ? fiber.sibling
    : fiber.parent.sibling
    ? fiber.parent.sibling
    : null;
}

最后在render函数中,初始化变量nextWorkOfUnit,这样我们的fiber架构就实现完成了

function 复制代码
  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el],
    },
  };
}

代码参考链接 : mini-react

参考文章

相关推荐
灵犀学长5 分钟前
解锁HTML5页面生命周期API:前端开发的新视角
前端·html·html5
江号软件分享14 分钟前
轻松解决Office版本冲突问题:卸载是关键
前端
致博软件F2BPM21 分钟前
Element Plus和Ant Design Vue深度对比分析与选型指南
前端·javascript·vue.js
慧一居士1 小时前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead1 小时前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码7 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子7 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年7 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子7 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina7 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试