The previous article mentioned that there is a scheduling step before the coordination, but this scheduling is no different with the reactdom.render API. React V17 is an interim release.

The Asynchronous interruptible Mode is used by the Scheduler to exert its power and is officially supported by React V18.

What is scheduling? Why is scheduling needed?

Generally speaking, the refresh rate of mainstream browsers is 60 Hz, which means that browsers refresh about every 16.6 ms (1000 ms / 60 Hz). Anything less than this frame rate will make people feel sluggish.

Browsers are single-threaded, so the JS thread and render thread cannot execute at the same time.

If the code execution time is long (more than 16.6ms), then a frame or frames will not have time to render due to the code execution, and then the frame will drop, and the stutter will be felt. React V15 and its earlier versions use a non-Fiber architecture, and all DOM nodes are traversed recursively. They cannot be interrupted and then resumed, so the code is executed synchronously, which leads to the problem of stalling.

After React V16, Fiber architecture was used to fragment tasks with long execution times. Each DOM node corresponds to a Fiber, and they are connected by the way of linked list, which can change the traversal mode to the depth traversal of linked list. By recording the current pointer, traversal can be paused and resumed.

If a task takes a long time and is not executed in one frame, then a short task is executed in each frame and then interrupted, leaving some time for the rendering process to work properly, taking some more time to continue the task in the next frame, and so on until the task is completed. This way the rendering of each frame is not blocked and we don’t feel stuck, and the process of fragmenting, executing, pausing, and executing again is called scheduling.

Scheduler

Scheduling of course requires a Scheduler, and Scheduler is a Scheduler that, when combined with time slicing, can allocate a runnable time to each unit of work based on the performance of the host environment to achieve “asynchronous interruptible updates”.

Scheduler has two functions:

  1. Time slice
  2. Priority scheduling

Time slice

In addition to browser rearrange/redraw, the order of times a browser frame can be used to execute JS is:

Macro Task -> Micro Task -> requestAnimationFrame -> Browser rearrange/redraw -> requestIdleCallback

RequestAnimationFrame executes JS before “browser rearrange/redraw”, which is the last time before the browser renders

RequestIdleCallback is called if the current frame has free time after “browser rearrange/redraw”

So the essence of time slicing is to simulate the implementation of requestIdleCallback.

Why mock the implementation, because requestIdleCallback is actually not very compatible with browsers, and MDN notes it as an experimental feature.

React actually uses macro task emulation to implement requestIdleCallback. This API will be used if the host environment supports MessageChannel, otherwise setTimeout will be used directly.

Priority scheduling

Scheduler is also independent of React packages, so its priority is independent of React priorities.

There are five priorities of Scheduler, corresponding to five different expiration times. Once a task expires, it needs to be executed immediately.

A large React project may have many tasks with different priorities at the same time. Some tasks with higher priorities are executed immediately, while others with lower priorities are executed delayed. The two tasks are assigned to different queues (timerQueue saves delayed tasks, and taskQueue saves immediately executed tasks).

TimerQueue is requeued every time a new delayed task is registered.

When the time is up for the task in timerQueue, we pull it out and add it to the taskQueue.

Finally, retrieve the earliest past task in the taskQueue and execute it.

In order to quickly find the first task that needs to be executed between the two queues, the Scheduler implements a small SchedulerMinHeap. As we know, the characteristics of the small top heap are that the value of each node is less than or equal to the value of each node in the subtree, that is to say, the top of the heap is the minimum value, and the top node in the corresponding Scheduler is the earliest task to be executed, and the time complexity of removing it is O(1).

The source code

How is scheduleCallback called

The call to the ReactDOM. Render API was described in the previous coordination article, but the scheduler still uses the ReactDOM. CreateRoot API to use Concurrent Mode. So the call process is a little bit different. The call process is roughly as shown in the figure below:

Can see the last is adjustable schedulerCallback and performConcurrentWorkOnRoot is this is actually the unstable_scheduleCallback Scheduler, Unstable means unstable and under development. It is not a feature in the official React V18 release.

Here’s a look at the Scheduler code, and the execution process is commented:

// path: /packages/scheduler/src/forks/Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // Get the current time
  var currentTime = getCurrentTime();

  // Generate start time, set delay to delay
  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;
  }

  var timeout;
  // 5 priorities and their corresponding 5 delay times. Configure the timeout period according to the priorities
  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;
  }

  // Set the expiration time
  var expirationTime = startTime + timeout;

  // Configure a new task
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1};if (enableProfiling) {
    newTask.isQueued = false;
  }

  // If the start time is longer than the current time, it indicates that the task is a delayed task and is directly added to timerQueue
  if (startTime > currentTime) {
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    // taskQueue is empty, the top element of timerQueue is the current task, indicating that the current task is the earliest of all tasks
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true; } requestHostTimeout(handleTimeout, startTime - currentTime); }}else {
    // If the start time is less than or equal to the current time, the start time is up. Directly join the taskQueue
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
      // Suspend the macro task to execute the render taskrequestHostCallback(flushWork); }}return newTask;
}
Copy the code

The requestHostCallback is a key call that selects which API to perform the macro task based on the host environment, SetImmediate > MessageChannel > setTimeout Why this order? Because the latency of execution is decreasing.

SetTimeout (fn, 0) does allow 4ms delay. SetImmediate mediate mediate does not mediate, but only IE and node.js support it. MessageChannel is supported on browsers like Chrome, but not on Internet Explorer.

FlushWork invocation process is complicated, can further see workLoop, performConcurrentWorkOnRoot function of logic, to sum up is the removal of taskQueue constantly in the earliest overdue tasks to perform, This is where resuming interrupt execution is implemented.

PerformUnitOfWork interrupt flag

Concurrent Mode, coordinate phase begins with performConcurrentWorkOnRoot, then transferred to the renderRootConcurrent – > workLoopConcurrent function.

// path: /packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while(workInProgress ! = =null&&! shouldYield()) { performUnitOfWork(workInProgress); }}Copy the code

ShouldYield () if true then the loop is broken. So let’s see what shouldYield is.

The first is to move down here.

// path: /packages/react-reconciler/src/Scheduler.js
import * as Scheduler from 'scheduler';

export const scheduleCallback = Scheduler.unstable_scheduleCallback;
export const cancelCallback = Scheduler.unstable_cancelCallback;
export const shouldYield = Scheduler.unstable_shouldYield;
export const requestPaint = Scheduler.unstable_requestPaint;
export const now = Scheduler.unstable_now;
export const getCurrentPriorityLevel =
  Scheduler.unstable_getCurrentPriorityLevel;
export const ImmediatePriority = Scheduler.unstable_ImmediatePriority;
export const UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
export const NormalPriority = Scheduler.unstable_NormalPriority;
export const LowPriority = Scheduler.unstable_LowPriority;
export const IdlePriority = Scheduler.unstable_IdlePriority;
export type SchedulerCallback = (isSync: boolean) = > SchedulerCallback | null;
Copy the code

ShouldYield -> shouldYield -> shouldYieldToHost.

// path: /packages/scheduler/src/forks/Scheduler.js
function shouldYieldToHost() {
  if( enableIsInputPending && navigator ! = =undefined&& navigator.scheduling ! = =undefined&& navigator.scheduling.isInputPending ! = =undefined
  ) {
    const scheduling = navigator.scheduling;
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      if (needsPaint || scheduling.isInputPending()) {
        return true;
      }
      const timeElapsed = currentTime - (deadline - yieldInterval);
      return timeElapsed >= maxYieldInterval;
    } else {
      return false; }}else {
    returngetCurrentTime() >= deadline; }}Copy the code

The main thing shouldYieldToHost does is call getCurrentTime to getCurrentTime and compare it to deadline. Return false if the current time is less than the cut-off time and continue the performUnitOfWork loop; If the current time is greater than or equal to the end time, you need to compare the difference between the two times and the yieldInterval time to determine whether you need to interrupt the execution of performUnitOfWork. The yieldInterval default value is 5 ms and is adjusted to the actual host environment. See the forceFrameRate function in Scheduler.js for details.

Therefore, whether performUnitOfWork can be executed depends on Scheduler’s command.

priority

The code for the priority-related definition is as follows:

// path: /packages/scheduler/src/SchedulerPriorities.js
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

// path: /packages/scheduler/src/forks/Scheduler.js
var maxSigned31BitInt = 1073741823;
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
Copy the code

In the “before Mutation” phase of the commit phase, we performed useEffect:

/ / path: packages/react - the reconciler/SRC/ReactFiberWorkLoop. New. Js
function commitRootImpl(root, renderPriorityLevel) {
  // Omit some code
    if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
      // Call useEffect asynchronously
      scheduleCallback(NormalSchedulerPriority, () = > {
        flushPassiveEffects();
        return null;
      });
    }
  // Omit some code
}
Copy the code

The callback is scheduled via scheduleCallback, and the priority is NormalSchedulerPriority, or NormalPriority. It can be delayed up to 5000ms, which is why useEffect was called so early, but it was executed very late.

Lane model

Lane model is the priority system of React, which is a strategy to control the relationship and behavior of different priorities.

Lane is a race track. Different cars run on different tracks, with shorter inner circuits and longer outer circuits. Some of the adjacent circuits can be regarded as the same length.

Lane model draws on the same concept and uses the binary of 31 to represent 31 tracks. The track with the smaller number of digits (that is, the closer to the right, the closer to the inner lap) has a higher priority, and some adjacent tracks have the same priority.

When Concurrent Mode is enabled: Expired tasks or synchronous tasks have the highest priority, using SyncLane circuit; Updates generated by user interaction (e.g., click events) use higher priority tracks; Network requests generate updates using general priority tracks; Suspense uses low priority tracks.

The source code

The circuit definition

SyncLane track has the highest priority, while OffscreenLane track has the lowest priority.

// path: packages/react-reconciler/src/ReactFiberLane.new.js
export const TotalLanes = 31;

export const NoLanes: Lanes = / * * / 0b0000000000000000000000000000000;
export const NoLane: Lane = / * * / 0b0000000000000000000000000000000;

export const SyncLane: Lane = / * * / 0b0000000000000000000000000000001;

export const InputContinuousHydrationLane: Lane = / * * / 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = / * * / 0b0000000000000000000000000000100;

export const DefaultHydrationLane: Lane = / * * / 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = / * * / 0b0000000000000000000000000010000;

const TransitionHydrationLane: Lane = / * * / 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = / * * / 0b0000000001111111111111111000000;
// Omit some code

const RetryLanes: Lanes = / * * / 0b0000111110000000000000000000000;
// Omit some code

export const SomeRetryLane: Lane = RetryLane1;

export const SelectiveHydrationLane: Lane = / * * / 0b0001000000000000000000000000000;

const NonIdleLanes = / * * / 0b0001111111111111111111111111111;

export const IdleHydrationLane: Lane = / * * / 0b0010000000000000000000000000000;
export const IdleLane: Lanes = / * * / 0b0100000000000000000000000000000;

export const OffscreenLane: Lane = / * * / 0b1000000000000000000000000000000;
// Omit some code
Copy the code

How are track priorities allocated to different events

In the use of createRoot this API to create a root node is called listenToAllSupportedEvents to add all event listeners, will eventually be transferred to the getEventPriority.

// path: packages/react-dom/src/events/ReactDOMEventListener.js
export function getEventPriority(domEventName: DOMEventName) : *{
  switch (domEventName) {
    case 'cancel':
    case 'click':
    // Omit some cases
    case 'drop':
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'invalid':
    case 'keydown':
    case 'keypress':
    case 'keyup':
    case 'mousedown':
    case 'mouseup':
    // Omit some cases
      return DiscreteEventPriority;
    case 'drag':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'mousemove':
    // Omit some cases
      return ContinuousEventPriority;
    case 'message': {
      const schedulerPriority = getCurrentSchedulerPriorityLevel();
      switch (schedulerPriority) {
        case ImmediateSchedulerPriority:
          return DiscreteEventPriority;
        case UserBlockingSchedulerPriority:
          return ContinuousEventPriority;
        case NormalSchedulerPriority:
        case LowSchedulerPriority:
          return DefaultEventPriority;
        case IdleSchedulerPriority:
          return IdleEventPriority;
        default:
          returnDefaultEventPriority; }}default:
      returnDefaultEventPriority; }}Copy the code

Event Priority definition:

// path: packages/react-reconciler/src/ReactEventPriorities.new.js
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;
Copy the code

Combining with the definition of event priority, it can be seen that the priorities of events are divided into four categories, which correspond to four categories of events: discrete events such as Click, KeyDown and MouseDown are all of high priority events. Consecutive events such as drag, mousemove, scroll take precedence.

It then sets the callback function that has the corresponding priority when the event is fired based on the priority of the event that it gets.

// path: packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags,) :Function {
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}
Copy the code

Priority correlation calculation

The calculation of track priority is a two-level bit operation.

// path: packages/react-reconciler/src/ReactFiberLane.new.js
// Determine whether two tracks overlap
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
  return(a & b) ! == NoLanes; }// Determine if a circuit is a subset of another circuit
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
  return (set & subset) === subset;
}
// The track or operation merges two tracks
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane) :Lanes {
  return a | b;
}
// Remove a subtrack from a track
export function removeLanes(set: Lanes, subset: Lanes | Lane) :Lanes {
  return set & ~subset;
}
// Get the intersection of two tracks
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane) :Lanes {
  return a & b;
}
Copy the code

These calculations will determine if the update is of sufficient priority when it is updated. If it is not, it may be skipped and other calculations related to priority are performed. If it is sufficient, it can be executed normally.

How does React use the Lane model

When we trigger the update, we call the requestUpdateLane function to get the current track, and the logic behind it decides how to do it based on the track information.

// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// Request an updated track
export function requestUpdateLane(fiber: Fiber) :Lane {
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
  } else if () {
    // Omit some code
  }
  // Omit some code
}
Copy the code

It can be seen that in non-ConcurrentMode mode, SyncLane track will be used, i.e. synchronous track. In Concurrent mode, the corresponding track is returned.

This is then converted to the priority of the corresponding Scheduler based on the situation of the track, thus driving the Scheduler’s scheduling.

// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // Omit some code

  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // Omit some code
    newCallbackNode = null;
  } else {
    let schedulerPriorityLevel;
    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;
    }
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

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