认识 React 中的时间切片

60 阅读2分钟

关于时间切片,本文将从以下三方面介绍:

  1. 什么是时间切片?
  2. 时间切片的作用是什么?
  3. 如何实现时间切片?

本文内容会使用堆的一些操作,可戳 最小堆 文章了解。

什么是时间切片?

其实就是一个时间段,比如 5ms。

时间切片的作用是什么?

避免高优先级任务被延迟执行,比如在输入框中输入关键字进行搜索,是高优先级的,如果被延迟执行,就会造成页面卡顿的现象,搜索结果和输入的关键字不一致。

如何实现时间切片?

先来了解几个概念。

任务(task)

类型如下:

type Task = {
  id: number;
  callback: Callback | null;
  priorityLevel: PriorityLevel;
  startTime: number;
  expirationTime: number;
  sortIndex: number;
};

执行任务,实际上就是执行任务的 callback 函数。

每个 task 都有一个 callback 函数,callback 执行完了,就执行下一个 task。

工作单元(work)

一个 work 就是一个时间切片内执行的一些 task。时间切片要循环,就是 work 要循环(loop)。

workLoop

workLoop 是实现时间切片的关键函数,顾名思义就是循环 work。

// 任务池,最小堆
const taskQueue: Array<Task> = [];

// 返回为 true,表示还有任务没有执行完,需要继续执行,返回 false 表示任务执行完了
function workLoop(initialTime: number): boolean {
  let currentTime = initialTime;
  // 从任务池里取出任务
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    // 过期时间大于当前时间,并且需要让出主线程(JS 是单线程),过期时间可以理解为开始执行的时间
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      break;
    }

    // 执行任务,其实就是执行它的 callback 函数
    const callback = currentTask.callback;
    if (typeof callback === "function") {
      // 有效的任务
      currentTask.callback = null; // 防止任务被重复执行
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 任务没有执行完
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === "function") {
        // 任务没有执行完,放在 callback 中,下次继续执行
        currentTask.callback = continuationCallback;
        return true;
      } else {
        // 任务执行完,并且是堆顶任务,直接删除
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
    } else {
      // 无效的任务,callback = null,直接删除
      pop(taskQueue);
    }

    currentTask = peek(taskQueue);
  }

  // while 循环完,看任务是否执行完
  if (currentTask !== null) {
    // 任务没有执行完
    return true;
  } else {
    // 任务执行完了
    return false;
  }
}

function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;

  if (timeElapsed < frameInterval) {
    return false;
  }

  return true;
}