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

65 阅读3分钟

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

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

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

image.png

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

requestIdleCallback

requestIdleCallback(callback[, options])

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

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

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

实现任务调度器

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

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

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) {}

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

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,我们将这两个步骤抽离出来:

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

image.png

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

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

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 = newFiber;
  });
}

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

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架构就实现完成了

  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el],
    },
  };
}

代码参考链接 : mini-react

参考文章