React scheduler
What is React Scheduler? This is how React can be used to assign tasks during diff. Js is single-threaded, so if too many tasks are executed at once, the browser may not respond if the user clicks a button, enters a number or something in between, and the user may think the browser is stuck. Now the browser provides an interface requesTidlecallBack, MDN description (developer.mozilla.org/en-US/docs/…) . The idea is that the browser now gives us the opportunity to perform a task when the browser is idle. Instead of using this interface, the React Scheduler implements a RequesTidlecallback ployFill of its own. Why didn’t it work that way? It’s said to be due to compatibility issues, or react doesn’t currently see strong support from browser vendors, or some other reason. React is implemented using the requestAnimationFrame to emulate the RequesTidlecallback.
The React the scheduler process
Here, all tasks are linked by a bidirectional linked list, similar to the figure below:
The React scheduler code
Code files (node_modules/scheduler/CJS/scheduler. Development. Js)
unstable_scheduleCallback
// Form a bidirectional linked list and start scheduling tasks
function unstable_scheduleCallback(callback, deprecated_options) {
// currentEventStartTime The initial value is -1, which is the current time of initial use
varstartTime = currentEventStartTime ! = =- 1
? currentEventStartTime
: exports.unstable_now();
// Expiration time
var expirationTime;
// Set the expiration time according to the priority
if (
typeof deprecated_options === "object"&& deprecated_options ! = =null &&
typeof deprecated_options.timeout === "number"
) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default: expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT; }}// Assemble a new node
var newNode = {
callback: callback,
priorityLevel: currentPriorityLevel,
expirationTime: 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) {
// This is the first callback in the list.
// Start arranging
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
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.
// Insert it in the back
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.
// Insert it in the front
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
Copy the code
Unstable_scheduleCallback is an alternative to requestIdleCallback. Unstable is not fixed and will be changed at any time. The unstable_scheduleCallback function is very simple. Based on the callback and options passed in, it calculates the expiration time and forms a two-way linked list of tasks. Then start by ensureHostCallbackIsScheduled () to arrange task execution cycles.
ensureHostCallbackIsScheduled
And then to see ensureHostCallbackIsScheduled the function, this is also very simple, first of all determine whether the task has started cycle arrangement and if it is, is quit, if not, the reset condition, start to arrange task request cycle.
// Have you started scheduling tasksfunction ensureHostCallbackIsScheduled() {// There is a callback in progressif (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield. return; } // Schedule the host callback using the earliest expiration in the list.var expirationTime = firstCallbackNode.expirationTime; if (! isHostCallbackScheduled) { isHostCallbackScheduled = true; } else {// Cancel the existing host callback cancelHostCallback(); } requestHostCallback(flushWork, expirationTime); }Copy the code
RequestHostCallback (flushWork, expirationTime); Flushwork is a task queue function in schedule that flushes tasks in a second. See requestHostCallback first
requestHostCallback
Here, the requestHostCallback determines which operations to perform next based on the incoming callback and expiration time. If a task is being executed that day, or the expiration time is less than zero, the requestHostCallback sends a message via port.postMessage to perform the task update immediately. Here the port.postmessage is
var channel = new MessageChannel();
var port = channel.port2;
Copy the code
It can be understood as a channel, that is, if a scheduler wants to update the task list immediately, it can send a message through port.postMessage, receive the message through channel.port1.onMessage, and immediately start the update of the task list. Like a publish subscription, when you want to update the list, you just send a message. Scheduler uses MessageChannel to notify and update the list of tasks. requestHostCallback Inside without due to the time and still haven’t begun to subscribe to the browser by isAnimationFrameScheduled free time, by requestAnimationFrameWithTimeout (animationTick) to subscribe.
RequestHostCallback = absoluteTimeout requestHostCallback =function(callback, absoluteTimeout) {scheduledHostCallback = callback; TimeoutTime = absoluteTimeout;if(isFlushingHostCallback | | absoluteTimeout < 0) {/ / / / Don ASAP as soon as possible't wait for the next frame. Continue working ASAP, in a new event. port.postMessage(undefined); } else if (! IsAnimationFrameScheduled) {/ / isAnimationFrameScheduled arrangement / / If the rAF didn 't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we // might want to still have setTimeout trigger rIC as a backup to ensure // that we keep performing work. isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); }};Copy the code
PostMessage (undefined) and channel.port1.onMessage, so I’m not going to look at this for a moment. Wait until the last look at the side of the code, the first look at the first requestAnimationFrameWithTimeout
requestAnimationFrameWithTimeout
RequestAnimationFrame is used here, but setTimeout is used when requestAnimationFrame does not work.
// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page // continues to load in the background. So we also schedule a 'setTimeout' as // a fallback. // TODO: Need a better heuristic for backgrounded work. var ANIMATION_FRAME_TIMEOUT = 100; var rAFID; var rAFTimeoutID; var requestAnimationFrameWithTimeout = function(callback) { // schedule rAF and also a setTimeout rAFID = localRequestAnimationFrame(function(timestamp) { // cancel the setTimeout localClearTimeout(rAFTimeoutID); callback(timestamp); }); rAFTimeoutID = localSetTimeout(function() { // cancel the requestAnimationFrame localCancelAnimationFrame(rAFID); callback(exports.unstable_now()); }, ANIMATION_FRAME_TIMEOUT); };Copy the code
The code is very simple. The callback passed in here is an animationTick. Take a look at the animationTick code
animationTick
The react function updates the time of each frame dynamically based on the actual FPS. The react function updates the time of each frame dynamically based on the actual FPS.
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz. // If the calculated frame time gets lower than 8, it is probably a bug. nextFrameTime = 8; } // If one frame goes long, then the next one can be short to catch up. // If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing. // We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime =
nextFrameTime < previousFrameTime
? previousFrameTime
: nextFrameTime;
Copy the code
As you can see, react also imposes a minimum limit of 8ms per frame
var animationTick = function(rafTime) {
if(scheduledHostCallback ! == null) { // Eagerly schedule the next animation callback at the beginning of the // frame. If the scheduler queue is not empty at the end of the frame, it // willcontinue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we // waited until the end of the frame to post the callback, we risk the // browser skipping a frame and not firing the callback until the frame // after that. requestAnimationFrameWithTimeout(animationTick); } else { // No pending work. Exit. isAnimationFrameScheduled = false; return; } var nextFrameTime = raftime-frameDeadline + activeFrameTime; if ( nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime ) { if (nextFrameTime < 8) { // Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we // actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're // running on 120hz display or 90hz VR display. // Take the max of the two in case one of them was an anomaly due to // missed frame deadlines. activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; } else { previousFrameTime = nextFrameTime; } frameDeadline = rafTime + activeFrameTime; if (! isMessageEventScheduled) { isMessageEventScheduled = true; port.postMessage(undefined); }};Copy the code
As you can see, port.postMessage(undefined) is used to trigger the task list queue.
channel.port1.onmessage
Now let’s see what happens when we actually start updating the task list. Code is relatively simple, the most important thing is to call here in the first ensureHostCallbackIsScheduled incoming requestHostCallback fulshWork (flushWork expirationTime), So prevScheduledCallback(didTimeout) in onMessage
// We use the postMessage trick to defer idle work until after the repaint channel = new MessageChannel(); var port = channel.port2; channel.port1.onmessage =function(event) {
isMessageEventScheduled = false; Var prevScheduledCallback = scheduledHostCallback; var prevTimeoutTime = timeoutTime; scheduledHostCallback = null; timeoutTime = -1; var currentTime = exports.unstable_now(); var didTimeout =false;
if (frameDeadline - currentTime <= 0) {
// There's no time left in this idle period. Check if the callback has // a timeout and whether it'S been exceeded. // The command has been executedif(prevTimeoutTime ! == -1 && prevTimeoutTime <= currentTime) { // Exceeded the timeout. Invoke the callback even though there's no // time left. didTimeout = true; } else {// No timeout. // If (! isAnimationFrameScheduled) { // Schedule another animation callback so we retry later. isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } // Exit without invoking the callback. scheduledHostCallback = prevScheduledCallback; timeoutTime = prevTimeoutTime; return; } } if (prevScheduledCallback ! == null) { isFlushingHostCallback = true; try { prevScheduledCallback(didTimeout); } finally { isFlushingHostCallback = false; }}};Copy the code
flushWork
At last we get to the real work, which is very simple: call the flushFirstCallback() at a given time through a loop and execute the most urgent callback at the end
function flushWork(didTimeout) { isExecutingCallback = true; Var previousDidTimeout = currentDidTimeout; currentDidTimeout = didTimeout; Try {if (didTimeout) {Yield Yield // Flush all the expired callbacks without yielding. While (firstCallbackNode! == null) { // Read the current time. Flush all the callbacks that expire at or // earlier than that time. Then read the current time again and repeat. // This optimizes for as few performance.now calls as possible. var currentTime = exports.unstable_now(); / / refresh list if (firstCallbackNode expirationTime < = currentTime) {do {flushFirstCallback (); } while ( firstCallbackNode ! == null && firstCallbackNode.expirationTime <= currentTime ); continue; } break; } } else { // Keep flushing callbacks until we run out of time in the frame. if (firstCallbackNode ! == null) { do { flushFirstCallback(); } while (firstCallbackNode! == null && ! shouldYieldToHost()); } } } finally { isExecutingCallback = false; currentDidTimeout = previousDidTimeout; if (firstCallbackNode ! == null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(); } else { isHostCallbackScheduled = false; } // Before exiting, flush all the immediate work that was scheduled. flushImmediateWork(); }}Copy the code
FlushFirstCallback = flushFirstCallback = flushFirstCallback = flushFirstCallback
// Update the first task
function flushFirstCallback() {
var flushedNode = firstCallbackNode;
// Remove the node from the list before calling the callback. That way the
// list is in a consistent state even if the callback throws.
var next = firstCallbackNode.next;
if (firstCallbackNode === next) {
// This is the last callback in the list.
// The last one, all set to null
firstCallbackNode = null;
next = null;
} else {
// Delete firstCallbackNode from the list
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
}
// Set all to null, independent
flushedNode.next = flushedNode.previous = null;
// Now it's safe to call the callback.
// Get various attributes
var callback = flushedNode.callback;
var expirationTime = flushedNode.expirationTime;
var priorityLevel = flushedNode.priorityLevel;
// Current level and expiration time, simple swap
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try {
continuationCallback = callback();
} finally {
/ / change back
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
}
// A callback may return a continuation. The continuation should be scheduled
// with the same priority and expiration as the just-finished callback.
// It is possible to return a function
if (typeof continuationCallback === "function") {
var continuationNode = {
callback: continuationCallback,
priorityLevel: priorityLevel,
expirationTime: expirationTime,
next: null.previous: null
};
// Insert the new callback into the list, sorted by its expiration. This is
// almost the same as the code in `scheduleCallback`, except the callback
// is inserted into the list *before* callbacks of equal expiration instead
// of after.
// Insert it according to the expiration time
if (firstCallbackNode === null) {
// This is the first callback in the list.
// Only one
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
} else {
var nextAfterContinuation = null;
var node = firstCallbackNode;
do {
// Compare expiration time,
if (node.expirationTime >= expirationTime) {
// This callback expires at or after the continuation. We will insert
// the continuation *before* this callback.
nextAfterContinuation = node;
break;
}
node = node.next;
} while(node ! == firstCallbackNode);// If not, it is inserted into the first one
if (nextAfterContinuation === null) {
// If empty, insert to the beginning of the list
// No equal or lower priority callback was found, which means the new
// callback is the lowest priority callback in the list.
nextAfterContinuation = firstCallbackNode;
} else if (nextAfterContinuation === firstCallbackNode) {
// The new callback is the highest priority callback in the list.
/ / if you have
firstCallbackNode = continuationNode;
ensureHostCallbackIsScheduled();
}
varprevious = nextAfterContinuation.previous; previous.next = nextAfterContinuation.previous = continuationNode; continuationNode.next = nextAfterContinuation; continuationNode.previous = previous; }}}Copy the code
#flushImmediateWork finally updates all the most urgent tasks,
function flushImmediateWork() {
if (
// Confirm we've exited the outer most event handler currentEventStartTime === -1 && firstCallbackNode ! == null && firstCallbackNode.priorityLevel === ImmediatePriority ) { isExecutingCallback = true; try { do { flushFirstCallback(); } while ( // Keep flushing until there are no more immediate callbacks firstCallbackNode ! == null && firstCallbackNode.priorityLevel === ImmediatePriority ); } finally { isExecutingCallback = false; if (firstCallbackNode ! == null) { // There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false; }}}}Copy the code
The code here seems to change with each react release, but the basic principles are pretty much the same and become little more than details.