React V17.3 Scheduler0.20.1

  • Time slice
  • Priority scheduling

The requestIdleCallback and requestAnimationFrame could not implement the React team’s requirements, so they implemented the Scheduler themselves.

  • Defects: Scheduling task delay, uncertain execution time, poor compatibility — issues-11330, issues-11171, issues-21662

Scheduling task – Create a macro task

Based on the event queue, creating macro tasks does not block the main thread, which is returned to the browser to perform the relevant rendering.

MessageChanel

It is equivalent to setTimeout of 0s delay, and supports Web Worker

const channel = new MessageChannel();
const port = channel.port2;
// Create the macro task with onMessage and execute performWorkUntilDeadline
The performWorkUntilDeadline function executes all tasks in the taskQueue
channel.port1.onmessage = performWorkUntilDeadline;
// Create a macro task that executes performWorkUntilDeadline
schedulePerformWorkUntilDeadline = () = > {
  port.postMessage(null);
};
Copy the code

setTimeout

This parameter is used when MessageChannel is not supported. In the browser, the actual minimum delay is 4s

schedulePerformWorkUntilDeadline = () = > {
  // setTimeout Executes performWorkUntilDeadline
  localSetTimeout(performWorkUntilDeadline, 0);
};
Copy the code

How many seconds are given for each macro task?

The default execution time is 5 seconds. It can also be dynamically adjusted by FPS (forceFrameRatereact is currently disabled). Determine whether the main thread function needs to be removed by subtracting the task start time from how long the main thread has been running.

const frameYieldMs = 5; // Block the main thread for 5 seconds
let frameInterval = frameYieldMs;

// Whether to give up the main thread. That is, determine whether the currently allocated execution time is used up
function shouldYieldToHost() {
  // getCurrentTime: getCurrentTime (performance or Date)
  // startTime: Each time performWorkUntilDeadline is assigned getCurrentTime()
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false;
  }
  // InputPending optimizations are ignored
 	// ...

  return true;
}
Copy the code

Priority scheduling

This priority is different from lane in React. In react, unstable_scheduleCallback is used for priority scheduling tasks

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // Get the current time (main thread executed time)
  var currentTime = getCurrentTime();
	// Start time of the task
  var startTime;
  // React does not use options. The startTime = currentTime
  if (typeof options === 'object'&& options ! = =null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else{ startTime = currentTime; }}else {
    startTime = currentTime;
  }

  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      // -1
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      / / 250
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      // maxSigned31BitInt 1073741823 Specifies the maximum integer size of a 32-bit system in V8.
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      / / 10000
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      / / 5000
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }
	// Task expiration time
  // The task will be executed if the expiration time is less than the current time.
  var expirationTime = startTime + timeout;
  // Task node
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1};// React does not use options, so does timerQueue
  if (startTime > currentTime) {
    // Add delayed tasks to timerQueue
    newTask.sortIndex = startTime;
    // Small top heap sort, the earliest expired first
    push(timerQueue, newTask);
    // If there are no tasks in taskQueue and newTask is timerQueu, the earliest one will expire
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        // If a scheduled task exists, it is cleared
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timer macro task handleTimeout
      // handleTimeout :
      // 1. Tasks whose startTime is smaller than currentTime in timerQueue are added to taskQueue
      RequestHostCallback (flushWork) if taskQueue is not empty;
      // If taskQueeu is null, the next timerQueue is scheduled. If taskQueeu is also empty, nothing is donerequestHostTimeout(handleTimeout, startTime - currentTime); }}else {
    // Add non-delayed tasks to the taskQueue
    newTask.sortIndex = expirationTime;
    // Small top heap sort, the earliest expired first
    push(taskQueue, newTask);
    if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
      // If the taksQueu scheduling function is not cleared,
      // Start scheduling flushWork
      // 
      // flushWork: all taks are executed separately.
      // 1. Expired TAks execution
      FlushWork 1,2. Until taksQueue is emptyrequestHostCallback(flushWork); }}return newTask;
}
Copy the code

Clear taskQueue — performWorkUntilDeadline

// Simplify the function, leaving only the core code
const performWorkUntilDeadline = () = > {
  ScheduledHostCallback with taks is assigned to flushWork
  // If the value is not empty, taks need to be scheduled
  if(scheduledHostCallback ! = =null) {
    const currentTime = getCurrentTime();
    
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
		  ScheduledHostCallback with taks is assigned to flushWork
      FlushWork: flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // If there are tasks (tasks that have not expired), continue scheduling
        // Will be scheduled until the taskQueue is empty
        schedulePerformWorkUntilDeadline();
      } else {
        scheduledHostCallback = null; }}}};// flushWork 
// Simplify the function, leaving only the core code
function flushWork(hasTimeRemaining, initialTime) {
  const previousPriorityLevel = currentPriorityLevel;
  try {
    // While executes taskQueue
    return workLoop(hasTimeRemaining, initialTime);
  } finally{ currentPriorityLevel = previousPriorityLevel; }}//workLoop 
// The simplified function.
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime); // Check timerQueue and move expired timers to taskQueue
  currentTask = peek(taskQueue); // Retrieve the first expired taskQueue
  while( currentTask ! = =null ) {
    if (
      // The execution time set by taks priority is not reached
      currentTask.expirationTime > currentTime && shouldYieldToHost())
    ) {
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // Remove callback from task, execute
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
      // If the return value is a function, taks updates the callback, and the continuation schedule is not pushed out of the taskQueue
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          // Task completes pushing taskQueuepop(taskQueue); }}// Check timerQueue and move expired timers to taskQueue
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    // Next task
    currentTask = peek(taskQueue);
  }
  if(currentTask ! = =null) {
    // If there are tasks, return true and performWorkUntilDeadline will continue
    // If taks is not executed in this context, the expirationTime of this context is not available
    return true;
  } else {
    // If there is no task, schedule timer
    // React does not have timer enabled
    const firstTimer = peek(timerQueue);
    if(firstTimer ! = =null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false; }}Copy the code

conclusion

ShouldYieldToHost is used to determine whether there is execution time, and unstable_scheduleCallback is used to schedule priorities. Create tasks to push the functions that need to be scheduled into the small top heap (sorted by time). Polls tasks whenever there are tasks (macro task polling) taskQueue If the task expires, the task is executed immediately. If not, the above polling is scheduled again until the taskQueue is empty.

Clear taskQueue by macro task polling to reduce main thread blocking.