Skip to content

RequestIdleCallback in Fiber

FPS 与页面流畅

  • fps,short for Frames Per Second,即每一秒的帧数。
  • fps 越大,浏览器越流畅。浏览器的 fps 一般等于屏幕的刷新率,不会超过屏幕的刷新率。

Frame 帧

a

b

浏览器 fps 通常为 60,每一帧执行完所有任务不能超过16.66ms(1000ms/60)。

一帧内需要完成如下六个步骤的任务:

  1. 处理用户的交互
  2. JS 解析执行
  3. 帧开始。窗口尺寸变更,页面滚去等的处理
  4. rAF
  5. 布局
  6. 绘制

RequestAnimationFrame

window.requestAnimationFrame() 会在浏览器重绘前调用传入的回调函数。

RequestIdleCallback

requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive.

只有当一帧的末尾有空闲时间,才会执行回调函数。

 window.requestIdleCallback(callback[, options])
  • callback:回调即空闲时需要执行的任务,接收一个 IdleDeadline 对象作为入参。
    • 只有当前帧的运行时间小于 16.66ms 时,callback 才会执行;否则,推迟到下一帧。以此类推。
    • 其中 IdleDeadline 对象包含:
      • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
      • timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
  • options:目前 options 只有一个参数 timeout 。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。
function sleep(delay) {
  let _start = Date.now();
  while(Date.now() - _start < delay) {}
}

// 任务队列
var start = Date.now();
var tasks = [
  () => {
    console.log("第1个任务开始");
    sleep(1000);
    console.log("第1个任务结束");
  },
  () => {
    console.log("第2个任务开始");
    sleep(300);
    console.log("第2个任务结束");
    console.log(Date.now() - start);
  },
];

function work() {
  tasks.shift()();
}

// 设置超时时间
var rIC = () => window.requestIdleCallback(runTask, { timeout: 1000 });

function runTask(deadline) {
  if (
    (
      deadline.timeRemaining() > 0 ||
      deadline.didTimeout
    ) &&
    tasks.length > 0
  ) {
    work();
  }

  if (tasks.length > 0) {
    rIC();
  }
}

rIC();

rIC vs rAF

  • rIC short for requestIdleCallback,rAF short for requestAnimationFrame。
  • rAF 的回调会在每一帧确定执行,属于高优先级任务;而 rIC 的回调则不一定,属于低优先级任务。
  • 对于用户,不那么重要的任务放在 rIC 中执行,如 react 的运算及调度
  • 而对于用户体验比较重要的任务(如:动画、click 事件任务等)放在 rAF 回调中执行。

React Scheduler

// fiber tree
let A1 = { key: 'A1' };
let B1 = { key: 'B1', return: A1 };
let B2 = { key: 'B2', return: A1 };
let C1 = { key: 'C1', return: B1 };
let C2 = { key: 'C2', return: B1 };
A1.child = B1;
B1.child = C1;
B1.sibling = B2;
C1.sibling = C2;

// React scheduler
let workInProgressRoot = null;
let nextUnitOfWork = null; // 下一工作单元
function sheduleRoot() {
  workInProgressRoot = A1;
  nextUnitOfWork = workInProgressRoot;
}

function workLoop(deadline) {
  let shoudYield = false; // 是否让出执行控制权
  while(nextUnitOfWork && !shoudYield) {
    nextUnitOfWork = performWorkofUnit(nextUnitOfWork);
    shoudYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && workInProgressRoot) {
    // commitRoot(workInProgressRoot);
    workInProgressRoot = null
  }
  // 进入死循环
  requestIdleCallback(workLoop, { timeout: 500 });
}

function performWorkofUnit(fiber) {
  beginWork(fiber);
  if (fiber.child) {
    return fiber.child;
  }

  while(fiber) {
    completeUnitOfWork(fiber);
    if (fiber.sibling) {
      return fiber.sibling;
    }
    fiber = fiber.return;
  }
}

requestIdleCallback(workLoop, { timeout: 500 });

function beginWork(fiber) {
  console.log('beginWork:', fiber.key);
}

function completeUnitOfWork(fiber) {
  console.log('completeUnitOfWork:', fiber.key);
}

// 每次渲染,执行一次 shedule
sheduleRoot();