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

参考文章

相关推荐
热爱编程的小曾26 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin37 分钟前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758102 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox