As you know, React’s vision is to respond quickly to users, fast enough to not block their interactions. As the React scheduling center, Scheduler ensures that high-priority tasks are executed first through priority division, time slice, interruptible and recoverable tasks and other strategies to improve performance. It can be called “time management master “, Luo Zhixiang this auspicious.

What is a Scheduler

Scheduler is a package built into the React project. You can assign tasks and their priorities to Scheduler to coordinate and schedule tasks. Currently Scheduler is only used for React, but the team’s vision is to make it more generic.

What does Scheduler do

Scheduler controls tasks from both macro and micro perspectives. Macroscopically, for multiple tasks, Scheduler arranges the execution order according to priority; For individual tasks (at the micro level), “measured” execution is required. What is “abstemious”? We know that JavaScript is single-threaded, and if a synchronization task takes a long time, it can lead to frame drops and stuttering. Therefore, you need to interrupt a time-consuming task in a timely manner to perform a more important task (such as user interaction), then perform the time-consuming task later, and so on. Scheduler uses such a mode to finely divide tasks to avoid using limited resources to execute time-consuming tasks and achieve faster responses.

The principle of review

In order to realize the management of multiple tasks and the control of a single task, Scheduler introduces two concepts: task priority and time slice. Task prioritization Prioritizes tasks in order of their urgency so that the highest priority tasks are executed first. The yieldInterval specifies the maximum execution time (5ms) for a single task within a frame. If the task execution time exceeds the yieldInterval, the task will be interrupted and switched to a higher-quality task. In this way, the page will not lose frames or affect user interaction because the task execution time is too long.

Management of multiple tasks

In Scheduler, tasks are divided into two types: unexpired tasks and expired tasks, which are stored in timerQueue and taskQueue respectively.

How do you distinguish between the two tasks

Compare task startTime (startTime) with currentTime (currentTime) :

  • whenstartTime > currentTime, indicating not expired, save totimerQueue
  • whenstartTime <= currentTime, indicating that it has expired and saved totaskQueue

How are the tasks to join the team ranked

Even when timerQueues and Taskqueues are separated, tasks in each queue have different priorities, so urgent tasks need to be ranked first according to their urgency. Older versions of React Scheduler use circular linked lists to concatenate objects. The code is difficult to understand and will not be expanded here.

The current source code using the small top heap this data structure implementation, heap is the priority queue of the bottom implementation, it in the insertion or deletion of elements, through the “float” and “sink” operation to make the elements automatically sort (priority queue is often used to solve the algorithm topK problem). Note that the heap’s elements are stored in arrays, not chains. The binary heap related logic will not be expanded in this article, interested can refer to my learning data structure and algorithm warehouse.

Going back to the source code, timerQueue and taskQueue ensure that elements are sorted from smallest to largest when inserting tasks. So what’s the basis for ordering?

  • In timerQueue, tasks are sorted according to startTime. The earlier the startTime, the earlier the description starts. Tasks with a smaller startTime are ranked first. By default, the start time of a task is the current time. If a delay time is sent when a task is scheduled, the start time is the sum of the current time and delay time.
  • In taskQueue, tasks are sorted by expirationTime. The earlier the expirationTime is, the more urgent the task is. The expiration time is calculated based on the task priority. The higher the priority is, the earlier the expiration time is.

Task execution

  • fortaskQueueBecause the tasks in the workLoop have expired, they need to be executed in a loop
  • fortimerQueueNone of the tasks in it will execute immediately, but will pass in the workLoop methodadvanceTimersMethod to check if the first task is out of date, and if so, totaskQueueIn the.

Task queue management is macro compared to individual task execution (more on that in a moment). The Lanes calculated from the React-Reconciler are converted into task priorities that Scheduler can recognize, which are then used to manage the order of tasks in the task queue. In short, the more urgent the task, the priority it needs to be.

Interruption and recovery of a single task

As the taskQueue executes each task, if a task takes too long to reach its time slice limit, the task must be interrupted to make way for something more important (such as browser drawing), and then resume when the high-priority expired task is complete. Scheduler needs two roles to achieve such scheduling effect: Scheduler of task and executor of task. The scheduler schedules an executor, who loops through the taskQueue to execute tasks one by one. When a task takes a long time to execute, the executor interrupts the task execution according to the time slice and then tells the scheduler: The task I am currently executing has been interrupted and some of it has not been completed, but it must now give way to more important things. Please assign another executor so that the task can be completed later. Therefore, the scheduler knows that the task has not been completed and needs to be continued, and it will assign another performer to continue to complete the task. Through the cooperation of executor and scheduler, the task can be interrupted and recovered. Suspending and resuming tasks is not a new concept, it has a term called coroutine, and generators after ES6 can use the yield keyword to simulate the concept of coroutines.

The source code parsing

This is the core principle of Scheduler, talk is cheap. To really understand it, you have to dig into the source code. I’ve created a branch to read the React source code, so you can review the whole thing on GayHub after reading the following.

React and Scheduler priority conversion

We know that React uses the Lane model for priority, while Scheduler is an independent package with its own priority mechanism, so a transformation is needed. This excerpt react – the reconciler/SRC/ReactFiberWorkLoop. Old (new). Part of the js.

let newCallbackNode;
/ / synchronize
if (newCallbackPriority === SyncLane) {
  // Execute the scheduleSyncCallback method
  // Just distinguish between Legacy mode and Concurrent mode
  ScheduleSyncCallback has its own syncQueue, which hosts synchronization tasks
  // flushSyncCallbacks handle synchronization tasks and send them to the following scheduleCallback
  // Make Scheduler schedule with the highest priority
  if (root.tag === LegacyRoot) {
    scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  }

  // Here we only talk about scheduleCallback, i.e., with the highest priority
  / / ImmediateSchedulerPriority tasks to perform synchronization
  if (supportsMicrotasks) {
    scheduleMicrotask(flushSyncCallbacks);
  } else {
    scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
  }
  newCallbackNode = null;
} else {
  / / asynchronous
  let schedulerPriorityLevel;
  // Lanes need to be converted to a priority that Scheduler can recognize
  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;
  }
  // Tasks and their priorities are passed to the Scheduler via scheduleCallback
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root)
  );
}
Copy the code

Priorities in Scheduler

Scheduler maintains six priorities, but NoPriority is not used on a review of the source code. They’re important for calculating expirationTime, and we know that expirationTime is about sorting taskQueues. This file is located in the scheduler/SRC/SchedulerPriorities. Js.

export const NoPriority = 0; // There is no priority
export const ImmediatePriority = 1; // The priority of immediate execution is the highest
export const UserBlockingPriority = 2; // Priority of user blocking level, such as user input, drag and drop
export const NormalPriority = 3; // Normal priority
export const LowPriority = 4; // Low priority
export const IdlePriority = 5; // The lowest priority, which can be left idle
Copy the code

scheduleCallback

From the above introduction, we know that the main entry of Scheduler is scheduleCallback, which is responsible for generating scheduled tasks, putting them into timerQueue or taskQueue based on whether the tasks are overdue, and then triggering the scheduling behavior for the tasks to be scheduled. Note: enableProfiling is used to do some auditing and debugger and is not covered in this article.

  1. First calculatestartTimeIt is used astimerQueueThe basis for sorting,getCurrentTime()To get the current time, as we’ll see below.
  2. Then calculateexpirationTimeIt is used astaskQueueThe expiration time is determined by the incoming priority.
  3. newTaskIs the data structure of the task unit in Scheduler, annotated clearly, wheresortIndexIs the basis for sorting in the priority queue (small top heap).
  4. According to the three steps above, this step is the basisstartTimecurrentTimePuts the task in a timerQueue or taskQueue, and triggers the scheduling behavior.
function unstable_scheduleCallback(priorityLevel, callback, options) {
  / * * (1 * /
  var currentTime = getCurrentTime();

  // timerQueue sorts by startTime
  // When a task enters, the start time is the current time by default
  // Start time is the sum of the current time and the delay time
  var startTime;
  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;
  }

  / * * (2 * /
  // taskQueue is sorted by expirationTime
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; / / 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 (2^ 30-1)
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; / / 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; / / 5000
      break;
  }

  // Calculate the expiration time of the task. The task start time + timeout
  IMMEDIATE_PRIORITY_TIMEOUT(-1) IMMEDIATE_PRIORITY_TIMEOUT(-1)
  // Its expiration time is starttime-1, which means it expires immediately
  var expirationTime = startTime + timeout;

  / * * (3 * /
  // Create a scheduling task
  var newTask = {
    id: taskIdCounter++,
    callback, // Scheduling tasks
    priorityLevel, // Task priority
    startTime, // The start time of the task, which indicates when the task can be executed
    expirationTime, // The expiration time of the task
    sortIndex: -1.// The basis for sorting in the small top heap queue
  };

  if (enableProfiling) {
    newTask.isQueued = false;
  }

  / * * (4 * /
  // startTime > currentTime Indicates that the task does not need to be executed immediately
  // Put it in timerQueue
  if (startTime > currentTime) {
    // timerQueue is determined by startTime.
    // Set startTime to sortIndex as the priority
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);

    // If taskQueue is empty and the current task has the highest priority
    // Then the task should be set to isHostTimeoutScheduled first
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // If the timeout schedule is already running, cancel it
      // Because the current task is the highest priority, the current task needs to be processed first
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.requestHostTimeout(handleTimeout, startTime - currentTime); }}else {
    // startTime <= currentTime Indicates that the task has expired
    // The task needs to be placed in the taskQueue
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);

    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }

    // If you are currently scheduling an expired task,
    // The current task needs to wait until the next time the slot is cleared
    if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true; requestHostCallback(flushWork); }}return newTask;
}
Copy the code

getCurrentTime

As the name implies, getCurrentTime is used to get the current time, using performing.now () in preference to date.now () otherwise. Performance.now () returns a DOMHighResTimeStamp(emMM) with a precision of millisecond.

let getCurrentTime;
const hasPerformanceNow =
  typeof performance === "object" && typeof performance.now === "function";

if (hasPerformanceNow) {
  const localPerformance = performance;
  getCurrentTime = () = > localPerformance.now();
} else {
  const localDate = Date;
  const initialTime = localDate.now();
  getCurrentTime = () = > localDate.now() - initialTime;
}
Copy the code

Looking at the Chromium source a little bit, it basically means that performing.now () is something that is monotonically increasing or less than IC_time, which ensures that the difference between calls is never negative; You also see some de-noising done with time_lower_digits and time_upper_digits to make sure the results aren’t too jarring. In addition, there is what coarsen time algorithm (Coarsen Time algorithm) is more difficult to understand.

DOMHighResTimeStamp Performance::now(a) const {
  return MonotonicTimeToDOMHighResTimeStamp(tick_clock_->NowTicks());
}
Copy the code

RequestHostTimeout and cancelHostTimeout

Apparently, these are two gay friends who love each other. In order for an unexpired task to reach a state that is exactly expired, delay startTime-currentTime by milliseconds. RequestHostTimeout is used to do this, and cancelHostTimeout is used to cancel the timeout function.

function requestHostTimeout(callback, ms) {
  taskTimeoutID = setTimeout(() = > {
    callback(getCurrentTime());
  }, ms);
}

function cancelHostTimeout() {
  clearTimeout(taskTimeoutID);
  taskTimeoutID = -1;
}
Copy the code

handleTimeout

The first argument to requestHostTimeout is handleTimeout, so let’s see what it does. The advanceTimers method is called first, which is used to update timerQueue and taskQueue. If timerQueue is found to be expired, it is put into taskQueue. Next, if no task is being scheduled, check whether the task exists in the taskQueue and flush it if it does. Otherwise requestHostTimeout(handleTimeout,…) is recursively executed. In short, this method is to move tasks from timerQueue to taskQueue.

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  // Update timerQueue and taskQueue
  // If timerQueue is found to be out of date, place it in taskQueue
  advanceTimers(currentTime);

  // Check whether scheduling has started
  // Do nothing if you are scheduling
  if(! isHostCallbackScheduled) {// If there are tasks in the taskQueue, execute the expired tasks first
    if(peek(taskQueue) ! = =null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // If there are no expired tasks, proceed to the highest priority of the first unexpired task
      // Continue the process until it can be placed on taskQueue
      const firstTimer = peek(timerQueue);
      if(firstTimer ! = =null) { requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); }}}}Copy the code

advanceTimers

This method is used to check expired tasks in timerQueue and put them in taskQueue. Mainly on the small top heap of various operations, directly read the notes.

function advanceTimers(currentTime) {
  let timer = peek(timerQueue);
  while(timer ! = =null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);

      // If the start time is less than or equal to the current time, the start time has expired.
      // Remove from taskQueue and place in taskQueue
    } else if (timer.startTime <= currentTime) {
      pop(timerQueue);
      // taskQueue is set to expirationTime.
      // The smaller the expirationTime is, the more urgent it is, and it should be placed at the top of a taskQueue
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);

      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true; }}else {
      // If the start time is greater than the current time, the task is still in timerQueue
      // The start time of a task is the current time by default. If a delay time is sent when the task is scheduled, the start time is the sum of the current time and delay time
      // The earlier the start time, the earlier the instructions will start, in front of the smallest heap
      // Remaining timers are pending.
      return; } timer = peek(timerQueue); }}Copy the code

requestHostCallback

Whether you’ve read the React source code or not, you’ve probably heard of the concepts of time slicing and task recovery after a task is interrupted. If it is “dispatch “, there must be a conductor and a worker.

Older React versions used requestAnimationFrame and requestIdleCallback to schedule tasks and align frames, but at [scheduler] Yield many times per frame, In no rAF #16214, this approach was abandoned. If you read my previous article dissecting requestAnimationFrame, rAF is subject to user behavior, such as switching tabs, scrolling pages, etc. If you look at the graph below, the slope of the first part is about 16.7, which is 1/60, but after I switched the tabs, the refresh rate was unstable immediately.

In addition, rAF, after all, depends on the refresh frequency of the display, while the refresh frequency on the market is at different levels, including 60Hz, like Apple ProMotion, which reaches 120Hz. In addition, good graphics cards are taken to mine, so it is really troublesome to be compatible. In short, this approach is subject to external factors that make the Scheduler less than 100 percent in control.

RequestIdleCallback, which will not be detailed, can be used to perform low-priority tasks during the browser idle phase without affecting the delay of critical events such as animations and input responses. For specific usage, please refer to the introduction on MDN.

In the current code, Scheduler uses MessageChannel to artificially control the scheduling frequency. The default time slice is 5ms, which is higher granularity than ProMotion. If you haven’t heard of MessageChannel before, you’ve probably heard of postMessage, which is often used to communicate between a host and an iframe. It’s also incredibly compatible.

Foreshadowing all said, direct look at the source code. It does a wave of compatibility, using setImmediate if node. js or low-end Internet Explorer does not expand. In a serious browser environment (IE: just read my ID card), we create an instance channel via MessageChannel, which has two ports that communicate with each other. Scheduler sends messages through port2 (port.postMessage) and receives messages through port1 (port1.onMessage). Therefore, port2 is the dispatcher, and Port1 is the one that receives the dispatch signal and does the real work.

let schedulePerformWorkUntilDeadline;

if (typeof setImmediate === "function") {
  schedulePerformWorkUntilDeadline = () = > {
    setImmediate(performWorkUntilDeadline);
  };
} else {
  const channel = new MessageChannel();
  const port = channel.port2;

  // Port1 receives the schedule signal to execute performWorkUntilDeadline.
  channel.port1.onmessage = performWorkUntilDeadline;

  // Port is the dispatcher (attack)
  schedulePerformWorkUntilDeadline = () = > {
    port.postMessage(null);
  };
}
Copy the code

RequestHostCallback assigns the incoming callback to the global variable scheduledHostCallback. If isMessageLoopRunning is false, it is enabled. Then a scheduling signal is sent to Port1 for scheduling.

function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if(! isMessageLoopRunning) { isMessageLoopRunning =true;

    // postMessage tells port1 to execute the performWorkUntilDeadline methodschedulePerformWorkUntilDeadline(); }}Copy the code

performWorkUntilDeadline

PerformWorkUntilDeadline is the executor of the task, that is, the function that port1 needs to execute once it receives the signal. It is used to execute the task within the timeslice, and if it is not finished, a new scheduler is used to continue the task. First, check whether scheduledHostCallback exists. If it exists, it indicates that there is a task to be scheduled. Calculate the deadline as the current time plus the yieldInterval(that is, the 5ms). See here will you suddenly understand, deadline actually do time slices! Next, set the constant hasTimeRemaining to true. “requestIdleCallback” is used to solve the problem. HasTimeRemaining is true because no matter if you complete the whole task or not, you have 5ms to solve the problem. You must have some time left in the time slice.

In summary, if the task is not completed within the time slice, the scheduler needs to ask another executor to continue to execute the task, otherwise the task is finished. The marker to determine the completion of a task is the hasMoreWork field, as described in workLoop below.

//
const performWorkUntilDeadline = () = > {
  if(scheduledHostCallback ! = =null) {
    const currentTime = getCurrentTime();
    // Time fragmentation
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;

    let hasMoreWork = true;
    try {
      // scheduledHostCallback to perform the real task
      // If true is returned, the current task is interrupted
      // The scheduler is asked to dispatch an executor to continue executing the task
      // The workLoop method will interrupt the workLoop
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // hasMoreWork is true if the task is interrupted
        // This is similar to recursion, so apply another scheduler to continue to execute the task
        schedulePerformWorkUntilDeadline();
      } else {
        // Otherwise, the current task is finished
        / / close isMessageLoopRunning
        // Set scheduledHostCallback to null
        isMessageLoopRunning = false;
        scheduledHostCallback = null; }}}else {
    isMessageLoopRunning = false;
  }
  // Yielding to the browser will give it a chance to paint, so we can
  // reset this.
  needsPaint = false;
};
Copy the code

flushWork

RequestHostCallback assigns flushWork to the scheduledHostCallback global variable. PerformWorkUntilDeadline also calls flushWork. Let’s see what flushWork is used for. FlushWork is to flushes tasks out in flushes, just as a taskQueue is a toilet that flushes tasks. Of course, the core of this method is to return workLoop.

function flushWork(hasTimeRemaining, initialTime) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // Because requestHostCallback does not necessarily execute the incoming callback immediately
  // So isHostCallbackScheduled may remain in place for a while
  // When the flushWork starts processing tasks, the state needs to be released in order for other tasks to be scheduled
  isHostCallbackScheduled = false;

  // Because taskQueue is already running
  // So there is no need to wait for tasks in timerQueue to expire
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if(currentTask ! = =null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        throwerror; }}else {
      // No catch in prod code path.
      returnworkLoop(hasTimeRemaining, initialTime); }}finally {
    // Restore the global state after executing the task
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      constcurrentTime = getCurrentTime(); markSchedulerSuspended(currentTime); }}}Copy the code

Task interruption and recovery — workLoop

At the end of the day, workLoop is a complete workLoop that can interrupt, recover, and determine the completion of a task.

  • Loop taskQueue to execute the task
  • Judgment of task status
    • If taskQueue completes, return false and take the highest priority from timerQueue to do the timeout scheduling
    • If the execution is not complete, the current schedule has been interrupted, return true, and the next schedule will continueperformWorkUntilDeadlineThe hasMoreWork)
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;

  TimerQueue and taskQueue need to be adjusted again because it is asynchronous
  advanceTimers(currentTime);

  // The most urgent expired task
  currentTask = peek(taskQueue);
  while( currentTask ! = =null &&
    !(enableSchedulerDebugging && isSchedulerPaused) // For the debugger
  ) {
    // Task interrupted!!
    // currentTask does not expire and breaks the loop
    // The current task is interrupted and needs to be executed in the next workLoop
    if( currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost()) ) {// This currentTask hasn't expired, and we've reached the deadline.
      break;
    }

    const callback = currentTask.callback;
    if (typeof callback === "function") {
      // Clear currenttask.callback
      // If the callback is empty in the next iteration, the task is finished
      currentTask.callback = null;

      currentPriorityLevel = currentTask.priorityLevel;

      / / has expired
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;

      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
      }

      // Execute the task
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();

      // If there is a continuous callback, there is an interruption
      // So the new continuationCallback is assigned to currenttask.callback
      // The next time the task is resumed, the callback will be connected
      if (typeof continuationCallback === "function") {
        currentTask.callback = continuationCallback;

        if(enableProfiling) { markTaskYield(currentTask, currentTime); }}else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        // If continuationCallback is not Function, the task is complete!!
        // If not, the task is finished and can be ejected
        if(currentTask === peek(taskQueue)) { pop(taskQueue); }}// The task above will take some time to update both queues again
      advanceTimers(currentTime);
    } else {
      // If clears currenttask.callback, so
      // If callback is empty, the task is finished and can be ejected
      pop(taskQueue);
    }

    // If the current task is finished, the next highest priority task is executed until the taskQueue is empty
    // if the currentTask is not completed, currentTask is still the currentTask, but callback becomes continuationCallback
    currentTask = peek(taskQueue);
  }

  // Task resume!!
  // The DDL has arrived, but the taskQueue is not finished yet (i.e. the task is interrupted)
  // return true, which is the flag of the recovery task
  if(currentTask ! = =null) {
    return true;
  } else {
    // If the task is complete!! Go to the timerQueue to find the task that needs to be started earliest
    // Run the requestHostTimeout command
    const firstTimer = peek(timerQueue);
    if(firstTimer ! = =null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false; }}Copy the code

shouldYieldToHost

There’s nothing more to this method than deciding whether to give up the main thread. But it lead to a more stylish API is the navigator. The scheduling. IsInputPending, it is used to don’t give up under the condition of the main thread to improve response ability, but Chrome 90 the API yet, presumably this is for the future. Better JS Scheduling with isInputPending()

function shouldYieldToHost() {
  if( enableIsInputPending && navigator ! = =undefined&& navigator.scheduling ! = =undefined&& navigator.scheduling.isInputPending ! = =undefined
  ) {
    const scheduling = navigator.scheduling;
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      // There's no time left. We may want to yield control of the main
      // thread, so the browser can perform high priority tasks. The main ones
      // are painting and user input. If there's a pending paint or a pending
      // input, then we should yield. But if there's neither, then we can
      // yield less often while remaining responsive. We'll eventually yield
      // regardless, since there could be a pending paint that wasn't
      // accompanied by a call to `requestPaint`, or other main thread tasks
      // like network events.
      // If you want to draw or have high priority I/O, you must give up the main thread
      if (needsPaint || scheduling.isInputPending()) {
        // There is either a pending paint or a pending input.
        return true;
      }
      // There's no pending input. Only yield if we've reached the max
      // yield interval.
      return currentTime >= maxYieldInterval;
    } else {
      // There's still time left in the frame.
      return false; }}else {
    // `isInputPending` is not available. Since we have no way of knowing if
    // there's pending input, always yield at the end of the frame.

    // Task execution exceeds DDL and the main process should be relinquished
    returngetCurrentTime() >= deadline; }}Copy the code

Cancel the schedule

Currenttask.callback = null; That is, the Scheduler determines that the task was canceled (or completed) using a null callback.

function unstable_cancelCallback(task) {
  if (enableProfiling) {
    if (task.isQueued) {
      const currentTime = getCurrentTime();
      markTaskCanceled(task, currentTime);
      task.isQueued = false; }}// Null out the callback to indicate the task has been canceled. (Can't
  // remove from the queue because you can't remove arbitrary nodes from an
  // array based heap, only the first one.)
  task.callback = null;
}
Copy the code

Custom time slice frequency

In order for Scheduler to become a separate package later, it opens the size of the set time slice, which defaults to 5ms and can be adjusted to between 0 and 125 depending on the situation. But how to handle this degree, I don’t know, I dare not ask.

function forceFrameRate(fps) {
  if (fps < 0 || fps > 125) {
    // Using console['error'] to evade Babel and ESLint
    console["error"] ("forceFrameRate takes a positive int between 0 and 125, " +
        "forcing frame rates higher than 125 fps is not supported"
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    // reset the framerate
    yieldInterval = 5; }}Copy the code

The last

The above is all the source code parsing of Scheduler, more than 20,000 words, most of which are code… In addition, there are some general logic packages in the source code, as well as some future-oriented features that are not covered in this article. If you are interested in GayHub, check out the source code. This article is based on V17.0.2, and there is no guarantee of what its code will look like in the future. See it first, enjoy it, and cherish it. If there is a major update later, I will try to update the article to keep it aligned with the master. Read the source code of this thing, not a matter of a day, nor can only one family, welcome everyone clap brick suggestions. It was a hard drawing and ended with a flowchart stolen from Shockw4ver.

reference

  • Priority management in React
  • React Scheduler
  • Explore the inner workings of React — postMessage & Scheduler
  • React schedulers React schedulers React schedulers
  • This is probably the most popular way to open React Fiber

See article: www.yanceyleo.com/post/b55fde…

Wechat public account: the front of the attack