React中任务调度机制和时间切片原理

528 阅读3分钟

react可以将大任务分解为多个小任务,分配到多个浏览器每帧下的空余时间执行,这就是我们常说的时间切片。这样就不会阻塞浏览器的绘制任务,造成页面卡顿。在这个过程中,react是如何实现任务的调度,并且如何实现时间切片的呢。

1.时间切片

浏览器如何控制react更新的呢。我们知道浏览器在绘制一帧的时候会处理很多事物,包括事件处理,js执行布局,绘制页面等等。

同时谷歌浏览器提供了requestIdleCallback API。这个api可以在“浏览器重排/重绘”后如果当前帧还有空余时间时被调用的。听起来这是个完美实现时间切片的api,但由于兼容性的问题。react并没有使用requestIdleCallback,而是模拟实现了requestIdleCallback,这就是Scheduler。

2.模拟requestIdleCallback

为了能模拟出requestIdleCallback,必须要做到以下两点。

  1. 可以主动让出线程,让浏览器执行其他任务。
  2. 在每帧下只执行一次,然后在下一帧中继续请求时间片。

能满足以上两种情况的便只有宏任务,而在宏任务中首选便是setTimeout。但是由于setTimeout会有4ms的时差,react放弃使用了setTimeout,改用了MessageChannel。(在不兼容Messagechannel的情况下依然使用setTimeout实现)

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

function schedulePerformWorkUntilDeadline() {
  port.postMessage(null);
}

3.任务调度

  1. 首先我们入口函数,就是我们外部调度需要调用这个函数,这里的关键点就是我们根据优先级,生成任务的过期时间和最小堆的排序依据,因为优先级更高,过期时间肯定越短。然后生成Task,塞到最小堆taskQueue中。然后开始去调用requestHostCallback函数调度任务。
function scheduleCallback(priorityLevel: PriorityLevel, callback: Callback) {
  const startTime = getCurrentTime();
  let timeout: number;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = -1;
      break;
    case UserBlockingPriority:
      timeout = userBlockingPriorityTimeout;
      break;
    case NormalPriority:
      timeout = normalPriorityTimeout;
      break;
    case LowPriority:
      timeout = lowPriorityTimeout;
      break;
    case IdlePriority:
      timeout = maxSigned31BitInt;
      break;
    default:
      timeout = normalPriorityTimeout;
  }
  const expirationTime = startTime + timeout;
  const newTask: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    //最小堆排序依据, 最小的在堆顶,过期时间越小,说明优先级越高
    sortIndex: expirationTime,
  };
  push(taskQueue, newTask);

  //只有一个主线程,看主线程是否在调度任务,时间切片是否在执行任务
  if (!isHostCallbackScheduled && !isPerformingWork) {
    isHostCallbackScheduled = true;
    requestHostCallback();
  }
}
  1. 调用 requestHostCallback 本质上就是发起了一次 MessageChannel 的调用,产生了一个宏任务。最终会执行workloop。
function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

function schedulePerformWorkUntilDeadline() {
  port.postMessage(null);
}

function performWorkUntilDeadline() {
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
    //记录一个Work的起始时间,其实就是一个时间切片的起始时间
    startTime = currentTime;
    let hasMoreWork = true;
    try {
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
}

  1. 在 workLoop 中会先检测一遍是否有任务过期,然后取出最先过期的任务执行。执行结果如果还是函数,就return出去,放在下一个宏任务执行, 如果不是函数则从任务队列清除这个任务, 最后再从任务队列取出新的任务, 开始while循环, 判断到如果任务到期了或者控制权应该给主线程,就跳出循环。
function workLoop(initialTime: number): boolean {
  let currentTime = initialTime;
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      break; // 如果任务的到期时间大于当前时间且应将控制权让给主机,则跳出循环
    }

    const callback = currentTask.callback;
    if (typeof callback === "function") {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      const continuationCallback = callback(didUserCallbackTimeout);
      if (typeof continuationCallback === "function") {
        currentTask.callback = continuationCallback;
        return true;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
    } else {
      pop(taskQueue);
    }

    currentTask = peek(taskQueue);
  }

  return false;
}

自己的理解

image.png