background

A key step in React is the asynchronous task scheduling logic, which is often asked in interviews. Read the source code to understand how React works and how asynchronous tasks, browsers, and idle time updates are linked together. This article is only about React task scheduling, not related concepts such as Fiber architecture setState and so on. React assigns tasks.

concept

After React16.5, a Scheduler is sent to a separate package called Scheduler. It is a module in React that can be understood as a separate module. The code version used below is version 16.6.0.

You can download from the official reference.

Download address

Official Concept Explanation

React Scheduler is used for collaborative scheduling in a browser environment.

The core logic

React emulates the requestIdleCallback, giving control to the browser to update the animation or user input during the main thread time. To schedule tasks that perform React itself that require rendering updates.

So let’s go through the core logic and summarize four important concepts.

  • Maintenance time slice
  • Expiration Time Priority
  • Analog implementationrequestIdleCallback
  • Scheduling lists and timeoutsrequestAnimationFrame

Let’s start with a little bit of explanation

Time slice concept

First, 1s is equal to 1000ms (ms). The ms unit will be millisecond. Then, the frame number is not fixed.

  • If 1s is equal to120 frames So 1 frame is equal to 8ms
  • If 1s is equal to60 frames So 1 frame is equal to 16 milliseconds
  • If 1s is equal to30 frames So 1 frame is equal to 33 milliseconds

In the case of 30 frames, we don’t feel stuck, so one frame is equal to a slice of time about 33ms

30 frames per second

If 1000/30 = 33ms, each time slice takes 33ms for each frame. If 11ms must be left for the browser to render its own animation, the remaining 22ms should be left for React rendering to ensure smooth rendering

If React rendering takes a long time (35ms) over 33ms, the browser will not have time to refresh its own animation, so the browser will have to use the next frame to refresh its own animation, resulting in a lag.

The ReactScheduler ensures that each frame does not exceed a certain time limit. We need to know what it does, right

The principle of Expiration Time priority & scheduleCallbackWithExpirationTime method logic

From scheduleCallbackWithExpirationTime name can know, is dispatching a Callback through ExpirationTime nature is the purpose, ExpirationTime to scheduling tasks

ReactFiberScheduler 1840 line around, you can observe the source code, accept FiberRoot and ExpirationTime two parameters, its main role is to compare ExpirationTime, and then according to the priority, different operations. About ExpirationTime before there is an article has already introduced its meaning, can be the following portal jump

Context Context Time(一) Context Time

function scheduleCallbackWithExpirationTime(root: FiberRoot, expirationTime: ExpirationTime,) {
  if(callbackExpirationTime ! == NoWork) {// Indicates that a callback has already been executed
    if (expirationTime > callbackExpirationTime) {
      // If this expirationTime is larger than the previous expirationTime, then this expirationTime has a lower priority
      return;
    } else {
      if(callbackID ! = =null) {
        // If the current expirationTime is smaller than the previous one, the current task has a higher priority, cancel the previous taskcancelDeferredCallback(callbackID); }}}else {
    startRequestCallbackTimer(); // Polyfill is not important here
  }

  callbackExpirationTime = expirationTime;
  const currentMs = now() - originalStartTimeMs; // Get the time difference between the current code loading and the current execution
  const expirationTimeMs = expirationTimeToMs(expirationTime); // The last update of expirationTime is converted to the ms value
  const timeout = expirationTimeMs - currentMs; // Future expiration time - the time taken to execute
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});  // scheduleDeferredCallback comes from the React task Scheduler
  // callbackID can be cancelled the next time it enters the method
  // performAsyncWork is the update method for synchronous task execution
}
Copy the code

React Scheduler Scheduling starts with scheduleDeferredCallback

Overall complete flow chart

First, show the overall content of the flow chart. It is recommended to understand the corresponding flow chart while reading it later, otherwise it is easy to be confused

scheduleDeferredCallback

// unstable_scheduleCallback is actually scheduleDeferredCallback consistent
export {
  unstable_now as now,
  unstable_scheduleCallback as scheduleDeferredCallback,
  unstable_shouldYield as shouldYield,
  unstable_cancelCallback as cancelDeferredCallback,
} from 'scheduler';
Copy the code
// scheduleDeferredCallback
function unstable_scheduleCallback(callback, deprecated_options) {
  varstartTime = currentEventStartTime ! = = -1 ? currentEventStartTime : getCurrentTime();

  varexpirationTime; .// We just need to focus on this part
    switch (currentPriorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT; // -1
        break;
      case UserBlockingPriority:
        expirationTime = startTime + USER_BLOCKING_PRIORITY; / / 250
        break;
      case IdlePriority:
        expirationTime = startTime + IDLE_PRIORITY; // Big Int The task that cannot be expired
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT; / / 5000
    }
    
  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null.previous: null};// Insert the new callback into the list, ordered first by expiration, then
  // by insertion. So the new callback is inserted any other callback with
  // equal expiration.
  if (firstCallbackNode === null) {
    // The node for the first callback
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    do {
      // expirationTime is larger than the current expirationTime
      if (node.expirationTime > expirationTime) {
        // Sort nodes by the size of their expirationTime
        next = node;
        break;
      }
      node = node.next;
    } while(node ! == firstCallbackNode);if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled(); / / received ensureHostCallbackIsScheduled next
    }
    
    // Form a circular list
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}
Copy the code

Scheduler’s scheduleDeferredCallback method (scheduleWork in scheduler packages) is used to asynchronously schedule root tasks.

We pass in the callback function performAsyncWork and an object containing the Timeout timeout event

Two things are happening here

  • FirstCallbackNodenullIn the case of its two Pointersnextprevious

  • FirstCallbackNodeAnd the newCallbackNodeThere,nextThe point is actually the next one that needs to be executedExpirationTimeThe largerNodeIf a new one appearsNodeThe node will then be inserted intoFirstCallbackNodeIt’s behind thepreviousnextWill point to the last oneNode

Here above can be understood as the preparation before dispatching

In preparation for task scheduling, build a chained data structure and register a task queue (circular list, head to tail, tail connector).

ensureHostCallbackIsScheduled

This method is prepared before scheduling

If the callback is already being called, return, because the call will continue, and isExecutingCallback will be set to true at flushWork

Set isHostCallbackScheduled to true if isHostCallbackScheduled is false, that is, it has not been scheduled yet, and cancel if it has, because the order may have changed.

Call requestHostCallback to formally begin scheduling

function ensureHostCallbackIsScheduled() {
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield. It means it's already pulling back
    return;
  }
  Schedule the host callback with the earliest expiration time in the list.
  var expirationTime = firstCallbackNode.expirationTime;
  if(! isHostCallbackScheduled) { isHostCallbackScheduled =true;
  } else {
    // Cancel the callback that was already in the queue
    cancelHostCallback();
  }
  
  // requestHostCallback
  requestHostCallback(flushWork, expirationTime); // Call Host Callback
}
Copy the code

requestHostCallback

Actually start scheduling

Start to enter the schedule, set the schedule content, use scheduledHostCallback and timeoutTime two global variables to record the callback function and the corresponding expiration time

Call requestAnimationFrameWithTimeout, is actually called requestAnimationFrame plus set up a 100 ms timer, prevent requestAnimationFrame too long don’t trigger.

The callback animtionTick and set isAnimationFrameScheduled global variable to true

requestHostCallback = function(callback, absoluteTimeout) {
  scheduledHostCallback = callback; // firstNode Callback
  timeoutTime = absoluteTimeout; // Expiration Time for firstCallBack
  // The method to be called next
  if (isFlushingHostCallback || absoluteTimeout < 0) {
    // This is out of time
    window.postMessage(messageKey, The '*'); // No need to wait for the next frame to do, directly postMessage, call into the method does not need to be scheduled
  } else if(! isAnimationFrameScheduled) {/ / isAnimationFrameScheduled determine whether has entered the scheduling process, if not then began to continue into the process
    If rAF has not arranged a frame, we need to arrange a frame.
    // TODO: If this rAF cannot be implemented due to browser limitations, we
    // You may still want to use the setTimeout trigger rIC as a backup to ensure that we continue to work.
    isAnimationFrameScheduled = true;
    // Prevent requestAnimationFrame from being called for longer than the default 100ms
    requestAnimationFrameWithTimeout(animationTick); // animationTick is mentioned in the method below}};Copy the code

requestAnimationFrameWithTimeout

The purpose of this method is to actually actually call requestAnimationFrame

Tell the browser that you want to execute an animation, and ask the browser to call the specified callback function to update the animation before the next redraw. This method takes as an argument a callback function that is executed before the browser’s next redraw

explain

RequestAnimationFrameWithTimeout, it is to address the page TAB if in inactive state requestAnimationFrame will not be triggered, in this way, the scheduler can be scheduled in the background, on the one hand, also can improve the user experience, At the same time, the interval of background execution is 100ms, which is a compromise interval that does not affect user experience and CPU power consumption

Points to be aware of

  • The system controls the timing of the callback to be executed at the beginning of the next frame rendering cycle after the callback registration is completed, controlling the accuracy of the JS calculation to the screen response to avoid out-of-step and resulting in frame loss
  • RequestAnimationFrame callbacks are executed only when the page is currently active, saving CPU overhead
  • It is important to note that if multiple requestAnimationFrame callbacks are registered at the same time during a high frequency interaction, the execution timing of these callbacks will be registered at the start of the next frame rendering cycle, resulting in increased rendering stress for each frame
  • The requestAnimationFrame callback argument is the time when the callback is invoked, which is the start time of the current frame
var ANIMATION_FRAME_TIMEOUT = 100; // 100ms
var rAFID;
var rAFTimeoutID;

var requestAnimationFrameWithTimeout = function(callback) {
  / / localRequestAnimationFrame is equivalent to the window. AnimationFrame API
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // Clear timer
    localClearTimeout(rAFTimeoutID);
    callback(timestamp); // Callback is the animationTick method
  });
  
  rAFTimeoutID = localSetTimeout(function() {
    // No call beyond 100ms will be cancelled
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime()); // Prevent requestAnimationFrame from not being called for longer
    // The first trigger will be called first
  }, ANIMATION_FRAME_TIMEOUT);
};
Copy the code

animationTick

As long as scheduledHostCallback still continue to adjust to requestAnimationFrameWithTimeout because it haven’t finished rendering may queue, itself is to enter the call again, This eliminates the need for requestHostCallback to be called this time

The next piece of code calculates the time difference between the requestAnimationFrames that are separated. If the time difference is less than the current activeFrameTime twice in a row, the platform frame rate is high, in which case the frame time must be dynamically reduced.

The frameDeadline is finally updated, and then the message is sent if idleTick is not fired

var animationTick = function(rafTime) {
  if(scheduledHostCallback ! = =null) {
    // This corresponds to the above judgment logic
    // Schedule the next animation callback in the frame
    // If the scheduler queue is not empty at the end of the frame, it continues to flush in that callback
    // If the queue is empty, exit immediately
    // Publish the frame callback at the beginning to ensure that it is fired within the earliest possible frame
    // We wait until the frame ends before issuing the callback, risking the browser skipping a frame until that frame triggers the callback
    requestAnimationFrameWithTimeout(animationTick);
  } else {
    // No more tasks need to be scheduled
    isAnimationFrameScheduled = false;
    return;
  }

  // Dynamically count frames
  // Calculate how long the current method can execute until the next frame
  var nextFrameTime = rafTime - frameDeadline + activeFrameTime; //
  
  // nextFrameTime previousFrameTime defaults to 33ms
  if (
    nextFrameTime < activeFrameTime &&
    previousFrameTime < activeFrameTime
  ) {
    if (nextFrameTime < 8) {
      // do not support the refresh time less than 8ms about 120hz
      nextFrameTime = 8;
    }
    // Determine the refresh frequency of the current platform
    // For example, if we are running on a 120Hz display or a 90Hz VR display.
    // Take the maximum of the two, in case one of them is abnormal due to the following reasons
    // The frame deadline was missed
    activeFrameTime =
      nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
      ActiveFrameTime is the complete time of a frame
  } else {
    previousFrameTime = nextFrameTime;
  }
  frameDeadline = rafTime + activeFrameTime;
  if(! isMessageEventScheduled) { isMessageEventScheduled =true;
    window.postMessage(messageKey, The '*'); / / window. PostMessage understanding}};Copy the code

Understanding window.postMessage

Because the requestIdleCallback API is still in the draft stage, the browser implementation rate is not high, so here React directly uses polyfill solution.

The simple solution is to use requestAnimationFrame to do some processing before the browser renders a frame, and then use postMessage to add a callback to the Macro Task (similar to setTimeout). So the main thread is held by the block and then comes back to empty the Macro Task. Is a task queue concept

In general, it is similar to requestIdleCallback, which is called when the main thread is free

After calling window.postMessage, the next call is idleTick

AnimationTick Triggers idleTick by postMessage

// Assumes that we have addEventListener in this environment. Might need
// something better for old IE.
window.addEventListener('message', idleTick, false);
Copy the code

idleTick

The actual function of this method is to schedule the previously defined task, compare the current timestamp with the previously defined time, and judge whether it is expired or not. It is a closed-loop process, judge and repeat the previous method

First check if postMessage is its own, not just a return

Clear scheduledHostCallback and timeoutTime

FrameDeadline = frameDeadline = frameDeadline = frameDeadline = frameDeadline = frameDeadline = frameDeadline = frameDeadline = frameDeadline If so, set didTimeout to true

The isFlushingHostCallback global variable is set to true to indicate that it is executing. And call callback which is flushWork and pass in didTimeout

var idleTick = function(event) {
  if(event.source ! = =window|| event.data ! == messageKey) {return;
  }

  isMessageEventScheduled = false;

  var prevScheduledCallback = scheduledHostCallback;
  var prevTimeoutTime = timeoutTime;
  scheduledHostCallback = null;
  timeoutTime = -1;

  var currentTime = getCurrentTime();

  var didTimeout = false;
  if (frameDeadline - currentTime <= 0) {
    // It took more than 33ms. There is no time left to update this frame
    if(prevTimeoutTime ! = = -1 && prevTimeoutTime <= currentTime) {
      If Timeout is less than the current time, the current task is also expired
      didTimeout = true; // You need to force an update
    } else {
      // There is no expiration date
      if(! isAnimationFrameScheduled) {// Schedule another animation callback so we retry later.
        isAnimationFrameScheduled = true;
        requestAnimationFrameWithTimeout(animationTick); // Re-execute the method so that it can go back to the previous step
      }
      // Return to the situation of the previous step
      scheduledHostCallback = prevScheduledCallback;
      timeoutTime = prevTimeoutTime;
      return; }}if(prevScheduledCallback ! = =null) {
    isFlushingHostCallback = true; // Callback is currently being called
    try {
       // Call Callback and pass didTimeout to determine if you need to force output
      prevScheduledCallback(didTimeout);
    } finally {
      isFlushingHostCallback = false; The next thing we need to call is the method for flushWork}}};Copy the code

flushWork

  • Do your top priority first
  • If there is a task, it goes to the next frame and enters the next scheduling life cycle

The callback function first executes all expired tasks, and then executes the callback until the frame time expires

Set isExecutingCallback to true to indicate that callback is being called

Set deadlineObject didTimeout, can be used to determine whether the task in the business of the React time out

If didTimeout, it executes backwards from firstCallbackNode once until the first unexpired task

If there is no timeout, the first callback is executed until the frame time expires

Finally clean up variable, if a task is not performed, are called again ensureHostCallbackIsScheduled into the schedule

Call all Immedia priority tasks by the way.

var deadlineObject = {
  timeRemaining,
  didTimeout: false};function flushWork(didTimeout) {
  isExecutingCallback = true; // Set to true after the actual Callback is called
  deadlineObject.didTimeout = didTimeout; // deadline
  try {
    // The firstTimeout Node task has expired
    if (didTimeout) {
      // Execute all expired tasks
      while(firstCallbackNode ! = =null) {
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          // This means that the list of expired tasks is executed until there are no expired tasks
          // 
          do {
            flushFirstCallback(); // The Callback method is actually called to execute all expired tasks
          } while( firstCallbackNode ! = =null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          continue;
        }
        break; }}else {
      // Keep the refresh callback until we run out of time in the frame.
      if(firstCallbackNode ! = =null) {
        do {
          flushFirstCallback(); // Call Callback as above
        } while( firstCallbackNode ! = =null &&
          getFrameDeadline() - getCurrentTime() > 0 // Frame time is still available); }}}finally {
    isExecutingCallback = false; // Set the status variable to indicate that it is complete
    if(firstCallbackNode ! = =null) {
      There is still work to be done. Request another callback to continue back to the logic above
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false; // Set the status variable to indicate that it is complete
    }
    Flush all scheduled jobs before exiting. This method hasn't been called yet and I won't expand on it hereflushImmediateWork(); }}Copy the code

flushFirstCallback

What does the method do?

  • If there is only one callback in the current queue, clear the queue
  • Call the callback and pass indeadlineObject, there aretimeRemainingMethods byframeDeadline - now()To determine if frame time is up
  • If the callback returns something, add the return to the callback queue
function flushFirstCallback() {
  var flushedNode = firstCallbackNode;

  // Remove nodes from the list before invoking the callback. This way, the list is in a consistent state even if a callback is raised.
  var next = firstCallbackNode.next;
  if (firstCallbackNode === next) {
    // This is the last callback in the list.
    firstCallbackNode = null;
    next = null;
  } else {
    var lastCallbackNode = firstCallbackNode.previous;
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  }

  // Update the list status
  flushedNode.next = flushedNode.previous = null;

 
  var callback = flushedNode.callback;
  var expirationTime = flushedNode.expirationTime;
  var priorityLevel = flushedNode.priorityLevel;
  var previousPriorityLevel = currentPriorityLevel;
  var previousExpirationTime = currentExpirationTime;
  currentPriorityLevel = priorityLevel;
  currentExpirationTime = expirationTime;
  var continuationCallback;
  try {
    continuationCallback = callback(deadlineObject); // Call the callback
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentExpirationTime = previousExpirationTime;
  }

  // The callback may return a continuation. Schedule to continue to have the same priority and expiration time as the callback you just completed.
  if (typeof continuationCallback === 'function') {
    var continuationNode: CallbackNode = {
      callback: continuationCallback,
      priorityLevel,
      expirationTime,
      next: null.previous: null};If the callback returns something, add it to the callback queue. I won't go into details here.Copy the code

conclusion

React emulates the requestIdleCallback core API to maintain timeslice operations. It controls giving more priority to the browser to animate or the user to feed back those updates, and then waiting for the browser to be free to perform React asynchronously, and there are a lot of conditions and variables to control the frame timing.

For example, if the browser refreshes more frequently, it will adjust the available time slice size per frame to a minimum of 8ms. For example, to determine whether a task has expired, you need to force the output to execute if it has expired.

Summary of process diagrams between methods

Before and after pictures

This is what happens when you use scheduling

That was the case before

Global variable reference

isExecutingCallback

FlushWork checks whether the callback method has been executed and sets it to true in flushWork and finally to false

isHostCallbackScheduled

Whether have begun to dispatch in ensureHostCallbackIsScheduled set to true, the callback after the carrying out of the end of the set to false

scheduledHostCallback

In the requestHostCallback setting, the value is typically flushWork, which represents what the next schedule will do

isMessageEventScheduled

Whether a message calling idleTick has been sent, set to true in animationTick

timeoutTime

Set when idleTick detects that the time of the first task has expired

isAnimationFrameScheduled

Whether requestAnimationFrame has been called

activeFrameTime

The time to render a frame is 33 by default, which is 30 frames per second

frameDeadline

The expiration time of the current frame is equal to currentTime + activeFraeTime, which is the time passed in by the requestAnimationFrame callback, plus the time of one frame.

isFlushingHostCallback

Whether a callback function method is being executed

This article refer to

React Scheduler Task Management