Before parsing the source code, we make clear a few questions:

  1. What is Scheduler and what does it do?
  2. What problem does Scheduler appear to solve?

Scheduler is a task Scheduler that calls tasks based on their priority. In the case of multiple tasks, it will execute the higher-priority tasks first. If a task takes too long to execute, the Scheduler interrupts the current task, freeing the thread from execution and preventing the interface from stalling during user operations. On the next recovery unfinished task execution.

Scheduler is a standalone package that is not only available in React.

The basic concept

This leads to two core points of Scheduler: task queue management and task interruption and recovery

Next, we will parse the source code with two questions:

  1. How is the task queue managed?
  2. How is the task executed, interrupted, and then resumed?

Detailed process

Reac puts the Fiber tree build into the scheduling process with the following code:

   function ensureRootIsScheduled(root: FiberRoot, currentTime: number){...let schedulerPriorityLevel;
       // The lanesToEventPriority function converts the lane priority to the Scheduler priority
       switch (lanesToEventPriority(nextLanes)) {
          case DiscreteEventPriority:
            schedulerPriorityLevel = ImmediateSchedulerPriority;
            break;
          case ContinuousEventPriority:
            schedulerPriorityLevel = UserBlockingSchedulerPriority;
            break;
          case DefaultEventPriority:
            schedulerPriorityLevel = NormalSchedulerPriority;
            break;
          case IdleEventPriority:
            schedulerPriorityLevel = IdleSchedulerPriority;
            break;
          default:
            schedulerPriorityLevel = NormalSchedulerPriority;
            break;
        }
        // Connect react to scheduler. Events generated by React are scheduled by scheduler as tasks
        newCallbackNode = scheduleCallback(
          schedulerPriorityLevel,
          performConcurrentWorkOnRoot.bind(null, root),
        );
   }
Copy the code

Why do we need to do a priority switch here? React and Scheduler are relatively independent and have their own internal priority mechanism. Therefore, when events generated by React need to be scheduled by Scheduler, the React event priority needs to be converted to Scheduler’s scheduling priority.

Scheduling entry -scheduleCallback

Next we click inside to see the scheduleCallback code:

function scheduleCallback(priorityLevel, callback) {...return Scheduler_scheduleCallback(priorityLevel, callback);
}
Copy the code
function unstable_scheduleCallback(priorityLevel, callback, options) {... }Copy the code

This method is the function that connects react to Scheduler.

Here’s a closer look at the basic configuration of Sheduler:

Priorities in Scheduler

export const NoPriority = 0; // No priority
export const ImmediatePriority = 1; // Priority of the task to be executed immediately, highest level
export const UserBlockingPriority = 2; // Priority of user blocking
export const NormalPriority = 3; // Normal priority
export const LowPriority = 4; // Lower priority
export const IdlePriority = 5; // The lowest priority, indicating that the task can be idle (idle tasks are executed only when there are no other tasks to execute)
Copy the code

Task management queues in Scheduler

Scheduler has two task queues: timerQueue and taskQueue. TimerQueue and taskQueue are both data structures of the smallest heap.

  1. TimerQueue: All unexpired tasks are placed in this queue.
  2. TaskQueue: All expired tasks are placed in this queue and sorted by expiration time. The smaller the expiration time, the higher the queue and the earlier the task is executed.

When the Scheduler starts scheduling tasks, the task is first fetched from the expired taskQueue, and a task is ejected from the taskQueue. When all tasks in the taskQueue are completed, The timerQueue is checked to see if there are any expired tasks, and the taskQueue is picked up and executed.

Here’s a look at the specific source code:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime(); // The current time

  var startTime; // Start time of task execution
  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; // The delay time of the task

  // The task timeout is given according to the priority of the task
  // The higher the priority, the smaller the time, and vice versa
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  var expirationTime = startTime + timeout; // Task expiration time

  // Use the react event to create a new task
  var newTask = {
    id: taskIdCounter++,
    callback, // callback = performConcurrentWorkOnRoot
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1};if (enableProfiling) {
    newTask.isQueued = false;
  }

  Scheduler has two task queues: timerQueue and taskQueue
  //timerQueue stores delayed tasks, that is, tasks that have not expired
  //taskQueue contains expired tasks, i.e. tasks that need to be executed immediately
  TimerQueue and taskQueue are minimal heap data structures

  // Compare the task start time with the current time
  // If the task start time is greater than the current time, the current task is a delayed task
  if (startTime > currentTime) {
    // This is a delayed task.
    // Use the start time as the sort ID. The smaller the start time, the higher the rank
    newTask.sortIndex = startTime;
    // Add the new task to the delayed task queue
    push(timerQueue, newTask);

    // When all the tasks in the expired task queue are finished,
    // The task in the delay queue must be iterated continuously. Once a task expires, the task must be added to the expired task queue for execution
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      // Check whether requestHostTimeout (that is, a setTimeout) is currently being executed. If yes, stop it to avoid unnecessary resource waste caused by multiple requestHostTimeout running together
      // Call requestHostTimeout again to check whether there are expired tasks in the delay queue
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      // Create a timeout as the schedulerrequestHostTimeout(handleTimeout, startTime - currentTime); }}else {
    // Use the expiration time as the sort ID, the smaller the order, the higher the order
    newTask.sortIndex = expirationTime;
    // Add the new task to the expired task queue
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    // All Scheduled tasks are Scheduled
    // If no, create a scheduler to start scheduling tasks. If yes, use the previous scheduler to start scheduling tasks
    if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true; requestHostCallback(flushWork); }}return newTask;
}
Copy the code

ScheduleCallback mainly creates a new task and determines whether the task is expired according to the start time of the task. If the task is not expired, it is added to timerQueue and startTimer is used as the ordering basis. If all tasks in a taskQueue have completed, requestHostTimeout is called. In effect, this function creates a setTimeout and calls handleTimeout as the setTimeout interval for the first task. So what does handleTimeout do? Let’s look at the source code:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;

  // Check whether there are expired tasks in the delayed task queue
  // If yes, add expired tasks to the expired task queue for execution
  advanceTimers(currentTime);

  / / isHostCallbackScheduled determine whether has been launched scheduling
  // If no schedule is currently running, a schedule is created to execute the task
  if(! isHostCallbackScheduled) {if(peek(taskQueue) ! = =null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      const firstTimer = peek(timerQueue);
      if(firstTimer ! = =null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); }}}}Copy the code

HandleTimeout checks to see if there are any expired tasks in timerQueue and adds expired tasks to taskQueue for execution. The advanceTimers function does this:

function advanceTimers(currentTime) {
  // Check whether there are expired tasks in the delayed task queue
  // If yes, add expired tasks to the expired task queue for execution
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while(timer ! = =null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true; }}else {
      // Remaining timers are pending.
      return; } timer = peek(timerQueue); }}Copy the code

For expired tasks, the expiration time is used as the basis for sorting, and the requestHostCallback function is called to create a scheduler to start the scheduling process.

if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
  requestHostCallback(flushWork);
}
Copy the code

Create the scheduler -requestHostCallback

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if(! isMessageLoopRunning) { isMessageLoopRunning =true; schedulePerformWorkUntilDeadline(); }}Copy the code

Callback is the flushWork function passed in by requestHostCallback, which will be called later. SchedulePerformWorkUntilDeadline real function is to create a dispatch, we’ll look at its implementation:

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  The main reason to use setImmediate is because MessageChannel prevents the NodeJS process from exiting while rendering on the server side
  schedulePerformWorkUntilDeadline = () = > {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeofMessageChannel ! = ='undefined') {
  // The reason for using MessageChannel is because
  // setTimeout if the nested level exceeds 5 levels and timeout is less than 4ms, setTimeout to 4ms.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () = > {
    port.postMessage(null);
  };
} else {
  // If none of the above solutions is possible, downgrade to setTimeout to create the scheduler
  schedulePerformWorkUntilDeadline = () = > {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}
Copy the code

I won’t cover setImmediate and MessageChannel in detail here, but I’ll come back to them later when I write a separate article on event loops.

SchedulePerformWorkUntilDeadline function mainly is to create a schedule, and call the performWorkUntilDeadline function for task scheduling. The performWorkUntilDeadline function calls the execution function of the task to start executing the task, so we’ll focus on executing, breaking, and resuming the task.

Task Execution -performWorkUntilDeadline

const performWorkUntilDeadline = () = > {
  if(scheduledHostCallback ! = =null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    
    try {
      ScheduledHostCallback = flushWork scheduledHostCallback = flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {

      //hasMoreWork is the last value returned by workLoop
      // Indicates whether there are tasks to be executed
      // If the value is true, a task is interrupted and needs to be executed again
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        //hasMoreWork is false to indicate that all tasks in the taskQueue are completed
        // The scheduler needs to be released in preparation for the next schedule
        isMessageLoopRunning = false;
        scheduledHostCallback = null; }}}else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};
Copy the code

The scheduledHostCallback function called inside performWorkUntilDeadline is assigned to flushWork when requestHostCallback is called.

Let’s see what happens in the flushWork function:

function flushWork(hasTimeRemaining, initialTime) {...returnworkLoop(hasTimeRemaining, initialTime); . }Copy the code

The workLoop function is finally called internally, and the wookLoop return value is returned, which is the value of hasMoreWork in performWorkUntilDeadline. You can see here that the actual task is performed in the wookLoop function.

Interrupt and resume tasks

There are two important features of task execution: interruption and recovery. Remember that when we introduced the function of Scheduler before, its main function is to check the execution time of tasks. If the execution time of tasks is too long, the task will be interrupted and the unfinished tasks will be resumed later. So how does it work? Let’s look at the structure of wookloop:

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // Check if there are expired tasks that need to be added to the taskQueue for execution
  advanceTimers(currentTime);

  // Retrieve the first task (add tasks according to the expiration time. The smaller the expiration time, the higher the execution priority.)
  currentTask = peek(taskQueue);

  while( currentTask ! = =null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    // Whether the current task expiration time is greater than the current time. If yes, the task does not expire and does not need to be executed immediately
    //hasTimeRemaining: Indicates whether there is any remaining time. If the remaining time is insufficient, interrupt the current task to allow other tasks to run first
    //shouldYieldToHost: Whether the current task should be interrupted
    if( currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost()) ) {break;
    }

    / / callback is performConcurrentWorkOnRoot function
    // Check whether callback is not empty. If it is empty, the current task will be removed from the task queue
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // The return function is null, indicating that the task is completed and will be deleted from the task queue
      currentTask.callback = null;
      // Get the priority of the task
      currentPriorityLevel = currentTask.priorityLevel;
      // Check whether the current task is expired
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // Get the result of executing the task
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // The result of the completed task returns a function indicating that the current task is not completed
        // Call this function as the new callback for the current task on the next While loop
        / / concurrent mode, the callback is performConcurrentWorkOnRoot function and its internal originalCallbackNode for the currently executing task
        // Compares the tasks mounted on root.callbackNode with those mounted on root.callbackNode. If they are different, the task is completed. If they are different, the task is not completed.
        // Returns itself as a new callback function for the current task, which then cedes execution to the higher-priority task
        currentTask.callback = continuationCallback;
      } else {
        if(currentTask === peek(taskQueue)) { pop(taskQueue); }}// Check whether there are expired tasks in the delay queue
      advanceTimers(currentTime);
    } else {
      // Delete the current task
      pop(taskQueue);
    }
    // Continue to fetch tasks from the taskQueue. If the last task was not completed, it will not be removed from the taskQueue
    // It will continue
    currentTask = peek(taskQueue);
  }

  CurrentTask returns true if the currentTask is interrupted.
  // Scheduler continues to initiate scheduling and execute tasks
  if(currentTask ! = =null) {
    return true;
  } else {
    //currentTask is null, indicating that all tasks in the taskQueue have been executed
    // Check if there are any tasks in timerQueue, and if there are any expired tasks
    // If so, add it to the taskQueue and resend the task
    const firstTimer = peek(timerQueue);
    if(firstTimer ! = =null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false; }}Copy the code

The wookloop function mainly loops taskQueue to perform tasks. The first task is assigned to the currentTask variable and the loop begins.

We see the first piece of code entering the loop:

if( currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost()) ) {break;
}
Copy the code

This code is the key to aborting the current mission.

Suspension judgment conditions:

  1. CurrentTask. ExpirationTime > currentTim: first, to judge whether the expiration time of the current task is greater than the current time, greater than, explain the current task has not expired don’t perform now, divides the task of executive power to have expired.
  2. ! HasTimeRemaining: Indicates whether there is any time left. If the remaining time is insufficient, you need to interrupt the current task to allow other tasks to run first. HasTimeRemaining is always true.
  3. ShouldYieldToHost function:
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false;
  }
 
  if (enableIsInputPending) {
    if (needsPaint) {
      return true;
    }
    if (timeElapsed < continuousInputInterval) {
      if(isInputPending ! = =null) {
        returnisInputPending(); }}else if (timeElapsed < maxInterval) {
      if(isInputPending ! = =null) {
        returnisInputPending(continuousOptions); }}else {
      return true; }}return true;
}
Copy the code

First check whether the current task usage time is less than the frame interval. If it is less than the frame interval, return false to indicate that no interruption is required.

StartTime is the value assigned when performWorkUntilDeadline is called, which is the startTime when the task is scheduled:

const performWorkUntilDeadline = () = >{... startTime = currentTime; . };Copy the code

If greater than indicates that the execution time of the current task exceeds the rendering time of a frame by 5ms, causing the user operation to stall, then return true to indicate that an interrupt is required.

IsInputPending is used to detect user input events, such as mouse click, keyboard input, etc. It returns true if there is user input and false if there is no user input.

Task execution

Next came the task:

/ / callback is performConcurrentWorkOnRoot function
// Check whether callback is not empty. If it is empty, the current task will be removed from the task queue
const callback = currentTask.callback;

if (typeof callback === 'function') {
  // The return function is null, indicating that the task is completed and will be deleted from the task queue
  currentTask.callback = null;
  // Get the priority of the task
  currentPriorityLevel = currentTask.priorityLevel;
  // Check whether the current task is expired
  const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
  if (enableProfiling) {
    markTaskRun(currentTask, currentTime);
  }
  // Get the result of executing the task
  const continuationCallback = callback(didUserCallbackTimeout);
  currentTime = getCurrentTime();
  if (typeof continuationCallback === 'function') {
    // The result of the completed task returns a function indicating that the current task is not completed
    // Call this function as the new callback for the current task on the next While loop
    / / concurrent mode, the callback is performConcurrentWorkOnRoot function and its internal originalCallbackNode for the currently executing task
    // Compares the tasks mounted on root.callbackNode with those mounted on root.callbackNode. If they are different, the task is completed. If they are different, the task is not completed.
    // Returns itself as a new callback function for the current task, which then cedes execution to the higher-priority task
    currentTask.callback = continuationCallback;
  } else {
    if(currentTask === peek(taskQueue)) { pop(taskQueue); }}// Check whether there are expired tasks in the delay queue
  advanceTimers(currentTime);
} else {
  // Delete the current task
  pop(taskQueue);
}
Copy the code

The currentTask task execution function callback is retrieved from the currentTask.

The callback is invoked scheduleCallback real incoming performConcurrentWorkOnRoot function:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {... newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), ); . }Copy the code

If not, the task will be removed from the taskQueue. If so, the callback will be executed. ContinuationCallback will return the result of execution. ContinuationCallback can be viewed as the execution state of the current task. When continuationCallback is null, it indicates that the current task is completed; if function, it indicates that the current task is not completed and interrupted during execution. You need to delegate execution to a higher priority task.

Execution status

So what about the execution state?

First let’s review the source code of the function ensureRootIsScheduled that calls scheduleCallback:

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {... newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root),
  );

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}
Copy the code

As you can see, a call to the scheduleCallback function returns the task object newTask to root’s callbackNode property

Then we enter a callback function, namely performConcurrentWorkOnRoot see exactly if judgment execution status of:

function performConcurrentWorkOnRoot(root, didTimeout) {
  constoriginalCallbackNode = root.callbackNode; .letexitStatus = shouldTimeSlice(root, lanes) && (disableSchedulerTimeoutInWorkLoop || ! didTimeout) ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}
Copy the code

CallbackNode is assigned to the originalCallbackNode variable, and renderRootConcurrent or renderRootSync is called. Then we can determine that root.callbackNode must be consumed in both methods.

You can then see that root.callbackNode is set to NULL during the commit phase:

function commitRootImpl(root, renderPriorityLevel) {... root.callbackNode =null; root.callbackPriority = NoLane; . }Copy the code

The React build process can be divided into two phases:

  1. Render phase, where missions can be interrupted
  2. Commit phase, where tasks cannot be interrupted

We come back look at performConcurrentWorkOnRoot function in the code:

function performConcurrentWorkOnRoot(root, didTimeout) {...if (root.callbackNode === originalCallbackNode) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}
Copy the code

CallbackNode is set to null, and continuationCallback in workloop will be null. Indicates that the task is complete.

What if the task is interrupted in the middle of execution?

Let’s look at the concurrent rendering function renderRootConcurrent:

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {...do {
    try {
      workLoopConcurrent();
      break;
    } catch(thrownValue) { handleError(root, thrownValue); }}while (true); . }Copy the code

Internally, the workLoopConcurrent function is called:

function workLoopConcurrent() {
  while(workInProgress ! = =null&&! shouldYield()) { performUnitOfWork(workInProgress); }}Copy the code

As you can see, this function loops through the workInProgress tree and calls the shouldYield function, which we parsed earlier to check if the execution time of the current task is longer than the render time of a frame. The isInputPendingAPI is used to determine whether the user is interacting with the page, and if one of the conditions is met, the current task is interrupted. If the task is interrupted, it will not proceed to the COMMIT stage, and root.callbackNode will not be null.

So root. CallbackNode will equal originalCallbackNode, then return to performConcurrentWorkOnRoot function will enter the if judgment.

So let’s go back to the code in workLoop:

// Get the result of executing the task
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
    // The result of the completed task returns a function indicating that the current task is not completed
    // Call this function as the new callback for the current task on the next While loop
    / / concurrent mode, the callback is performConcurrentWorkOnRoot function and its internal originalCallbackNode for the currently executing task
    // Compares the tasks mounted on root.callbackNode with those mounted on root.callbackNode. If they are different, the task is completed. If they are different, the task is not completed.
    // Returns itself as a new callback function for the current task, which then cedes execution to the higher-priority task
    currentTask.callback = continuationCallback;
} 

// Check whether there are expired tasks in the delay queue
advanceTimers(currentTime);

// Continue to fetch tasks from the taskQueue. If the last task was not completed, it will not be removed from the taskQueue
// It will continue
currentTask = peek(taskQueue);
Copy the code

When the current task is not completed, it is not removed from the taskQueue. Instead, the returned function continuationCallback is reassigned to the callback attribute of the current task, which is then checked to see if any expired tasks need to be executed during execution, and if so, added to the taskQueue.

If the current task is interrupted because it takes too long to execute, peek(taskQueue) retrieves the previous unfinished task and continues executing.

If the interruption is caused by a higher-priority task, peek(taskQueue) pulls out the highest-priority task for execution.

When currentTask is null or is interrupted by a condition that determines the task:

while( currentTask ! = =null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if( currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost()) ) {break; }... }Copy the code

So go to the following:

if(currentTask ! = =null) {
    return true;
  } else {
    //currentTask is null, indicating that all tasks in the taskQueue have been executed
    // Check if there are any tasks in timerQueue, and if there are any expired tasks
    // If so, add it to the taskQueue and resend the task
    const firstTimer = peek(timerQueue);
    if(firstTimer ! = =null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
Copy the code

When currentTask is not null, the currentTask returns true, indicating that there are still tasks in the taskQueue that need to be scheduled.

When currentTask is null, the currentTask returns false, indicating that all tasks in the taskQueue have completed, and the taskQueue needs to be checked to see if there are any more tasks in the timeQueue. If there are any more tasks in the timeQueue, the taskQueue needs to be added to the taskQueue when the first task in the timeQueue expires. And since the previous scheduling has ended, we need to create a new scheduler to initiate task scheduling:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;

  // Check whether there are expired tasks in the delayed task queue
  // If yes, add expired tasks to the expired task queue for execution
  advanceTimers(currentTime);

  / / isHostCallbackScheduled determine whether has been launched scheduling
  // If no schedule is currently running, a schedule is created to execute the task
  if(! isHostCallbackScheduled) {if(peek(taskQueue) ! = =null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      const firstTimer = peek(timerQueue);
      if(firstTimer ! = =null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); }}}}Copy the code

We are looking at the performWorkUntilDeadline function, which assigns the return value to hasMoreWork when the workLoop completes:

const performWorkUntilDeadline = () = > {
    try {
      ScheduledHostCallback = flushWork scheduledHostCallback = flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      //hasMoreWork is the last value returned by workLoop
      // Indicates whether there are tasks to be executed
      // If the value is true, a task is interrupted and needs to be executed again
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        //hasMoreWork is false to indicate that all tasks in the taskQueue are completed
        // The scheduler needs to be released in preparation for the next schedule
        isMessageLoopRunning = false;
        scheduledHostCallback = null; }}}Copy the code

If hasMoreWork is true, tasks are not executed and a new scheduler needs to be created to initiate task scheduling.

When hasMoreWork is false, all tasks are completed, and isMessageLoopRunning and scheduledHostCallback are reset to prepare for the next schedule.

At this point, the whole Scheduler scheduling process ends.

conclusion

Sheduler sets expirationTime expiration by task priority and then stores tasks to timerQueue and taskQueue respectively by determining whether tasks are expired. Expired tasks are removed from timerQueue and placed in taskQueue for execution.

Scheduler creates schedulers differently in different environments:

  1. The server uses setImmediate to create the dispatcher
  2. MessageChannel is used on the browser side to create the scheduler
  3. When none of the above schemes is possible, the demotion uses setTimeout to create the scheduler

Why use different apis to create schedulers in different environments?

Using MessageChannel on the server prevents the Node process from closing. Using setImmediate does not.

The reason why you don’t use setTimeout directly to create a scheduler is because setTimeout sets timeout to 4ms if the nested hierarchy is more than 5 levels and timeout is less than 4ms. The render time of a frame is 16.6ms, and using setTimeout by default will use 4ms, which is unacceptable for browsers.

Tasks are executed using a While loop called taskQueue, which determines whether to interrupt the current task based on whether the current task has been executed for longer than one frame of rendering time and whether the user is interacting with the interface. If the task is interrupted, a function is returned. If the task is not interrupted, null is returned. The returned value determines the execution status of the task and determines whether to resume the execution of the interrupted task.