Click on the React source debug repository.
As an independent package, Scheduler can take on the responsibility of task scheduling alone. You only need to assign tasks and task priorities to it, and it can help you manage tasks and schedule their execution. This is how React and Scheduler work together.
For multiple tasks, it executes the higher-priority ones first. For individual tasks, it performs them sparingly. In other words, there is only one thread, and it does not occupy the thread to perform tasks all the time. You do it for a while, break it, and so on and so forth. In this mode, users can avoid using limited resources to perform time-consuming tasks, solve the problem of page lag during user operations, and achieve faster response.
We can tease out two important behaviors in Scheduler: the management of multiple tasks and the execution control of a single task.
The basic concept
In order to realize the above two behaviors, it 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 time slice defines the maximum execution time of a single task within this frame. Once the execution time of a task exceeds the time slice, it will be interrupted and the task will be executed in moderation. This ensures that the page does not get stuck because tasks are executed consecutively for too long.
The principle of overview
Based on the concepts of task priority and time slice, Scheduler derives two core functions around its core objective – task scheduling: task queue management and interruption and recovery of tasks under time slice.
Task queue Management
Task queue management corresponds to the multitasking management behavior of Scheduler. Within Scheduler, tasks are divided into two types: unexpired and expired, which are stored in two queues, the former in timerQueue and the latter in taskQueue.
How do I tell if a task is expired?
Compare the task startTime (startTime) with the currentTime (currentTime). If the start time is greater than the current time, it is not expired and is placed in timerQueue. If the start time is less than or equal to the current time, the start time has expired and is placed in taskQueue.
How are tasks sorted in different queues?
When tasks are queued one by one, they are naturally sorted to ensure that the urgent tasks come first, so the sorting is based on the urgency of the task. The criteria for determining the urgency of tasks in taskQueue and timerQueue are different.
- 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.
- 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.
The task enters two queues, and then what?
If placed in a taskQueue, a function is immediately scheduled to loop through the taskQueue, executing each task in turn.
If it’s in the timerQueue, then none of the tasks in the timerQueue will execute immediately, so wait until the time when the first task in the timerQueue starts to see if the task is out of date, if so, remove the task from the timerQueue and put it in the taskQueue, schedule a function to loop through it, [Fixed] Execute quests inside Otherwise, check later to see if the first task is expired.
Task queue management is a macro-level concept compared with the execution of individual tasks. It uses the priority of tasks to manage the order of tasks in the task queue, and always lets the most urgent tasks be processed first.
Interruption and recovery of individual tasks
The interruption of a single task and the resumption of control over the execution of a single task corresponding to the Scheduler. 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, to complete and resume execution.
In this example, click the button to render 140,000 DOM nodes in order to allow React to schedule a lengthy update task through the Scheduler. Drag the box at the same time, this is to simulate user interaction. The update task takes up threads to execute the task, and the user interaction takes up threads to respond to the page, which makes them mutually exclusive. In React Concurrent mode, update tasks scheduled by Scheduler encounter user interaction, which will look like the following GIF.
The React task execution and page response interaction are mutually exclusive, but since Scheduler can interrupt the React task with a timeslath and then release the thread for the browser to draw, dragging blocks gets immediate feedback at first during the Fiber tree construction phase. However, there was a lag later because the Fiber tree was completed and the synchronization phase was committed, which caused the interaction to lag. The rendering process of the analysis page can be seen very intuitively through the control of time slices. The main thread is left out to do the Painting and Rendering of the page (green and purple).
Scheduler needs two roles to achieve such scheduling effect: the Scheduler of tasks and the executor of tasks. The scheduler schedules an executor, who loops through the taskQueue to execute tasks one by one. When a task execution time is longer, executives are executed according to the time slice interrupt tasks, and then tell the caller: I’m now on execution of this task was interrupted, part but also not to complete, but now I have to give way to something more important, your scheduling another practitioner, so this task can continue to be performed after the recovery (task). 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.
The principle of summary
Scheduler manages both the taskQueue and timerQueue queues. It periodically places expired tasks from timerQueue into the taskQueue and then has the Scheduler tell the executor to cycle the taskQueue to execute each task. The executor controls the execution of each task, once the execution time of a task exceeds the time slice. Will be interrupted, then the current executor exits, exit will notify the caller to go before scheduling a new practitioners continue to complete the task, the new executive mission will continue according to the time slice interrupt tasks, then exit, repeating the process, until the current, after the completion of the task to complete the task from taskQueue team. Each task in the taskQueue is processed in this way, and all tasks are completed, which is the complete workflow of the Scheduler.
One of the key points here is how does the actor know if the task is complete or not? That’s a whole other topic, which is judging the status of the task. The details of how the performer performs the task will be highlighted.
The above is an overview of the Scheduler principle, and the following is a detailed explanation of how React and Scheduler work together. It covers the connection between React and Scheduler, scheduling entry, task priority, task expiration time, task interruption and recovery, and task completion status.
Detailed process
Before we begin, let’s take a look at the React and Scheduler system.
The whole system is divided into three parts:
- Where tasks are generated: React
- React and Scheduler communication translator: SchedulerWithReactIntegration
- Scheduler of tasks: Scheduler
React uses the following code to schedule fiber tree builds:
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
Copy the code
Tasks are handed to Scheduler by a translator, and Scheduler does the actual task scheduling, so why do you need a translator role?
React connects to Scheduler
Scheduler helps React schedule tasks, but essentially they are two completely uncoupled things, each with its own priority mechanism, so an intermediate role is needed to connect them.
In fact, in the react – the reconciler provides such a file to do such work, it is SchedulerWithReactIntegration. Old (new). Js. It translates their priorities so React and Scheduler can read each other. In addition, some Scheduler functions are encapsulated for use with React.
React task in the implementation of the important documents ReactFiberWorkLoop. Js, concerning the content of the Scheduler is from SchedulerWithReactIntegration. Old (new). Js import. It can be understood as a bridge between React and Scheduler.
// ReactFiberWorkLoop.js
import {
scheduleCallback,
cancelCallback,
getCurrentPriorityLevel,
runWithPriority,
shouldYield,
requestPaint,
now,
NoPriority as NoSchedulerPriority,
ImmediatePriority as ImmediateSchedulerPriority,
UserBlockingPriority as UserBlockingSchedulerPriority,
NormalPriority as NormalSchedulerPriority,
flushSyncCallbackQueue,
scheduleSyncCallback,
} from './SchedulerWithReactIntegration.old';
Copy the code
SchedulerWithReactIntegration. Old (new). Js by encapsulating the content of the Scheduler, provide two kinds of scheduling the entrance to the React function: scheduleCallback and scheduleSyncCallback. Tasks enter the scheduling process through scheduling entry functions.
For example, the fiber tree construction task is scheduled by scheduleCallback in Concurrent mode and by scheduleSyncCallback in synchronous rendering mode.
// concurrentMode
// Change the priority of the update task to the scheduling priority
// schedulerPriorityLevel indicates the scheduling priority
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority,
);
/ / concurrent mode
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
// Synchronize render mode
scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
)
Copy the code
In fact, both of them are the encapsulation of scheduleCallback in Scheduler, but the priority passed in is different. The former transmits the scheduling priority calculated by lane that has been updated this time, while the latter transmits the highest priority. Another difference is that the former will directly to the Scheduler, which put the task in SchedulerWithReactIntegration first. The old (new). Js own synchronous queue, and the function of synchronous queue to the Scheduler, with the highest priority scheduling, The passing of the highest priority means that it will be an immediately expired task and will be executed immediately, ensuring that the task will be executed in the next event loop.
function scheduleCallback(
reactPriorityLevel: ReactPriorityLevel,
callback: SchedulerCallback,
options: SchedulerCallbackOptions | void | null.) {
// Translate the react priority to the Scheduler priority
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
// Call the Scheduler's scheduleCallback and pass in the priority for scheduling
return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
function scheduleSyncCallback(callback: SchedulerCallback) {
if (syncQueue === null) {
syncQueue = [callback];
// Schedule functions that refresh syncQueue with the highest priority
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueueImpl,
);
} else {
syncQueue.push(callback);
}
return fakeCallbackNode;
}
Copy the code
Priorities in Scheduler
Speaking of priorities, let’s take a look at Scheduler’s own priority level, which defines the following levels of priority for tasks:
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 the user's blocking level
export const NormalPriority = 3; // Normal priority
export const LowPriority = 4; // Lower priority
export const IdlePriority = 5; // The task has the lowest priority and can be idle
Copy the code
The role of task priority has already been mentioned. It is an important basis for calculating the expiration time of tasks, and it is related to the ordering of expired tasks in the taskQueue.
// Different priorities correspond to different task expiration intervals
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
varIDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; .// Calculate expiration time (in scheduleCallback)
var timeout;
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;
}
// startTime is the current time
var expirationTime = startTime + timeout;
Copy the code
It can be seen that the expiration time is the start time of the task plus timeout, which is calculated from the priority of the task.
A more comprehensive explanation of priorities in React is available in this article I wrote: Priorities in React
Scheduling entry – scheduleCallback
Through the above combing, we know that scheduleCallback in Scheduler is the key point at the beginning of scheduling process. Before entering the scheduling portal, let’s take a look at what tasks in Scheduler look like:
var newTask = {
id: taskIdCounter++,
// Task function
callback,
// Task priority
priorityLevel,
// The start time of the task
startTime,
// The expiration time of the task
expirationTime,
// The basis for sorting in the small top heap queue
sortIndex: -1};Copy the code
- Callback: the real task function, key, also is the function of the incoming tasks, such as the construction of fiber tree task functions: performConcurrentWorkOnRoot
- PriorityLevel: specifies the priority of a task, which is used to calculate the task expiration time
- StartTime: indicates the startTime of the task, which affects its order in timerQueue
- ExpirationTime: Indicates when a task expires, affecting its order in a taskQueue
- SortIndex: Sort a task in a small top heap queue. After you distinguish expired or non-expired tasks, sortIndex is given a expirationTime or startTime to sort two small top heap queues (taskQueue,timerQueue)
The real focus is callback. As a task function, its execution result will affect the judgment of task completion state. We will talk about it later, so it is not necessary to pay attention to it for the time being. Now let’s take a look at what scheduleCallback does: it generates scheduled tasks, puts them in a timerQueue or taskQueue based on whether the task is overdue, and then triggers scheduling behavior to put the task on schedule. The complete code is as follows:
function unstable_scheduleCallback(priorityLevel, callback, options) {
// Get the current time, which is used to calculate the start time and expiration time of the task, and determine whether the task is expired
var currentTime = getCurrentTime();
// Determine the start time of the task
var startTime;
// Try to get delay from options
if (typeof options === 'object'&& options ! = =null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
// If there is delay, the task start time is the current time plus delay
startTime = currentTime + delay;
} else {
// There is no delay, the task start time is the current time, that is, the task needs to start immediatelystartTime = currentTime; }}else {
startTime = currentTime;
}
/ / calculate the timeout
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 ms
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
// If the ImmediatePriority is ImmediatePriority,
// Its expiration time is starttime-1, which means it expires immediately
var expirationTime = startTime + timeout;
// Create a scheduling task
var newTask = {
id: taskIdCounter++,
// Real task function, emphasis
callback,
// Task priority
priorityLevel,
// The start time of the task, which indicates when the task can be executed
startTime,
// The expiration time of the task
expirationTime,
// The basis for sorting in the small top heap queue
sortIndex: -1};// If... Else judge the meaning of each branch:
// If the task is not expired, place newTask in timerQueue, call requestHostTimeout,
// The purpose is to check whether the task is expired at the start time of the first task in the timerQueue.
// If the task expires, the task will be added to the taskQueue immediately
// If the task has expired, place the newTask in the taskQueue, call requestHostCallback,
// Start scheduling tasks in taskQueue
if (startTime > currentTime) {
// Tasks are not expired. TimerQueue is sorted by the start time
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// If there are no tasks in the taskQueue and the current task is the highest ranked task in the timerQueue
// Check if there are any tasks in timerQueue that need to be added to taskQueue by calling timerQueue
/ / requestHostTimeout implementation
if (isHostTimeoutScheduled) {
// Because a requestHostTimeout is scheduled, cancel it if it has already been scheduled
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Call requestHostTimeout to transfer tasks and enable schedulingrequestHostTimeout(handleTimeout, startTime - currentTime); }}else {
// the task has expired. The taskQueue is sorted by the expiration time
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
FlushWork is used to execute taskQueue
if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true; requestHostCallback(flushWork); }}return newTask;
}
Copy the code
The focus of this process is the handling of expired tasks.
For unexpired tasks, the timerQueue will be placed in order by the start time and requestHostTimeout will be called in order to wait for the start time of the first task in the timerQueue to check whether it is expired. If it expires, it is put into the taskQueue so that the task can be executed, otherwise continue to wait. This is done with handleTimeout.
HandleTimeout is responsible for:
- call
advanceTimers
Check for expired tasks in timerQueue and place them in taskQueue. - Check whether the taskQueue has already been scheduled. If not, check whether there are tasks in the taskQueue:
- If there are, and they are now idle, it indicates that the previous advanceTimers have placed expired tasks in the taskQueue and are now scheduled to execute the tasks immediately
- If not, and is now idle, the previous advanceTimers did not detect expired tasks in timerQueue and are called again
requestHostTimeout
Repeat the process.
In short, all tasks in timerQueue must be transferred to taskQueue for execution.
For an expired task, after it is placed in a taskQueue, requestHostCallback is called so that the scheduler dispatches an executor to execute the task, which means that the scheduling process begins.
Start scheduling – Find the scheduler and the performer
Scheduler calls requestHostCallback to get the task into the scheduling process, recalling where scheduleCallback finally calls requestHostCallback to perform the task above:
if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
// Start scheduling
requestHostCallback(flushWork);
}
Copy the code
FlushWork is used as an entry to the flushWork task, so the executor of the task will essentially call flushWork. Let’s ignore how the executor executes the task and focus on how it is scheduled.
Scheduler distinguishes between browser and non-browser environments and makes two different implementations for requestHostCallback. In a non-browser environment, use setTimeout.
requestHostCallback = function(cb) {
if(_callback ! = =null) {
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0); }};Copy the code
In the browser environment, it is implemented using MessageChannel, which will not be covered here.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if(! isMessageLoopRunning) { isMessageLoopRunning =true;
port.postMessage(null); }};Copy the code
There are two types of implementation, because the browser does not exist the screen refresh rate of the environment, without the concept of frame, also won’t have time, to perform a task in a browser environment has essential difference, because the browser basic didn’t have the user interaction of the environment, so the scenario does not determine whether the task execution time is beyond the time limit, The execution of browser environment tasks is limited by time slices. Other than that, the two environments implement things differently, but they do much the same thing.
In the non-browser environment, it stores the input parameter (the function that performs the task) to the internal variable _callback, and then dispatches _flushCallback to execute the variable _callback. The taskQueue is cleared.
Looking at the browser environment, it stores the input parameter (the function that performs the task) to the internal variable scheduledHostCallback, and then sends a message via the MessageChannel port, Make channel.port1’s listener performWorkUntilDeadline execute. PerformWorkUntilDeadline will execute the scheduledHostCallback internally and the taskQueue will be cleared.
From the above description, it is clear to identify the scheduler: setTimeout in the non-browser environment and Port.postMessage in the browser environment. The executors of the two environments are also obvious. The former is _flushCallback and the latter is performWorkUntilDeadline. All the executors do is call the actual task execution function.
Since this paper focuses on the scheduling behavior of Scheduler’s time slice, it mainly discusses the scheduling behavior in the browser environment. PerformWorkUntilDeadline involves calling the task execution function to execute the task, which involves the interruption and recovery of the task and the judgment of the task completion state. The following sections will focus on these two points.
Task execution – start with performWorkUntilDeadline
PerformWorkUntilDeadline is mentioned in the principle overview at the beginning of this article as the executor. Its function is to interrupt the task according to the time slice limit and inform the scheduler to schedule a new executor again to continue the task. When you look at the realization of this realization, it’s very clear.
const performWorkUntilDeadline = () = > {
if(scheduledHostCallback ! = =null) {
// Get the current time
const currentTime = getCurrentTime();
// Calculate deadline, deadline will participate
// shouldYieldToHost is in the calculation
deadline = currentTime + yieldInterval;
// hasTimeRemaining indicates whether the task has any time left,
// It, along with time slices, limits the execution of tasks. If you don't have time,
// If the execution time of the task exceeds the time limit, the task is interrupted.
// It defaults to true, meaning there is always time left
// Since MessageChannel's port is in postMessage,
// is a macro task executed before setTimeout, which means
// At the beginning of the frame, there is always time left
// So now we interrupt the task and only watch the time slice
const hasTimeRemaining = true;
try {
// scheduledHostCallback to perform the task function,
// It returns true when the task is interrupted due to a time slice
// There are tasks, so the scheduler will be asked to schedule an executor
// Continue to perform tasks
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if(! hasMoreWork) {// Stop scheduling if there are no more tasks
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there are still tasks, continue to let the scheduler schedule the executor so that it can continue
// Complete the task
port.postMessage(null); }}catch (error) {
port.postMessage(null);
throwerror; }}else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
Copy the code
PerformWorkUntilDeadline internally calls scheduledHostCallback, which is assigned to flushWork by requestHostCallback at the beginning of the schedule, You can review the implementation of requestHostCallback above.
FlushWork, the function that actually executes tasks, loops through the taskQueue, calling each task function in it one by one. Let’s see what flushWork does.
function flushWork(hasTimeRemaining, initialTime) {...returnworkLoop(hasTimeRemaining, initialTime); . }Copy the code
It calls workLoop and returns the result of its call. Now the core of the task execution appears to be in the workLoop. The workLoop call causes the task to be executed.
Task interruption and recovery
To understand workLoop, recall that one of Scheduler’s functions is to limit the execution time of tasks through time slices. Then, since the execution of the task is restricted, it must be incomplete, and if the unfinished task is interrupted, it needs to be resumed.
So task execution under the timeslice has the following important characteristics: it can be interrupted and it can be resumed.
It’s not hard to guess that workLoop, as the function that actually executes the task, must be doing something related to the interrupt recovery of the task. Let’s take a look at its structure:
function workLoop(hasTimeRemaining, initialTime) {
// Get the first task in the taskQueue
currentTask = peek(taskQueue);
while(currentTask ! = =null) {
if(currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost())) {// break the while loop
break}...// Execute the task.// The task is deleted from the queue
pop(taskQueue);
// Get the next task and continue the loop
currentTask = peek(taskQueue);
}
if(currentTask ! = =null) {
// If currentTask is not empty, it is time slice limitation that causes the task to interrupt
// return a true that tells the outside world that the task has not yet been completed.
// hasMoreWork
return true;
} else {
// If currentTask is empty, tasks in the taskQueue have been completed
// When it's done, call requestHostTimeout to retrieve the task from timerQueue
// Put the task in the taskQueue, and it will be scheduled again, but this time,
// Return false to indicate that the current taskQueue has been cleared.
// Stop the task execution, that is, terminate the task scheduling
const firstTimer = peek(timerQueue);
if(firstTimer ! = =null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false; }}Copy the code
WorkLoop can be divided into two parts: loop taskQueue task execution and task status determination.
Loop taskQueue to execute the task
Regardless of how the task is executed, just focus on how the task is bounded by the time slice. In workLoop:
if(currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost())) {// break the while loop
break
}
Copy the code
CurrentTask is a currently executing task that terminates if: The task is not expired, but there is no time left (because hasTimeRemaining is always true, which depends on the timing of the MessageChannel as a macro task, we ignore this condition and only look at the timeslath), or it should be ceded execution to the main thread (the timeslath constraint), CurrentTask (currentTask, currentTask, currentTask, currentTask, currentTask, currentTask, currentTask, currentTask) However, only the while loop is broken, and the lower part of the while still determines the currentTask state.
CurrentTask cannot be null because it is only aborted, and returns true to tell the external that the task has not completed, otherwise all tasks have completed, the taskQueue has been cleared, and the taskQueue has been cleared. Return false to allow the external to terminate the schedule. FlushWork (scheduledHostCallback) flushWork (scheduledHostCallback) When performWorkUntilDeadline detects that the return value of scheduledHostCallback (hasMoreWork) is false, the schedule is stopped.
Reviewing the behavior in performWorkUntilDeadline, it is clear to string together the mechanism for task interrupt recovery:
const performWorkUntilDeadline = () = >{...const hasTimeRemaining = true;
// scheduledHostCallback to perform the task function,
// It returns true when the task is interrupted due to a time slice
// There are tasks, so the scheduler will be asked to schedule an executor
// Continue to perform tasks
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if(! hasMoreWork) {// Stop scheduling if there are no more tasks
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there are still tasks, continue to let the scheduler schedule the executor so that it can continue
// Complete the task
port.postMessage(null); }};Copy the code
When a task is interrupted, performWorkUntilDeadline tells the scheduler to call an executor to continue executing the task until it is complete. But the big point here is how do you tell if the task is done? This requires exploring the part of the workLoop logic that performs the task.
Determine the completion status of individual tasks
Interrupt recovery of a task is a repetitive process that is repeated until the task is complete. So it is important to determine whether the task is complete, and if it is not, the task function is repeated.
We can use the analogy of a recursive function, which calls itself repeatedly if it doesn’t reach the recursive boundary. And this recursive boundary is the completion of the task. Since the recursive function processes the task itself, it is convenient to take the task completion as the recursive boundary to end the task. However, the workLoop in Scheduler is different from recursion in that it only performs the task and the task is not generated by itself. It is external (for example, it performs the React work loop rendering fiber tree). It can execute the task function repeatedly, but the boundary (i.e., whether the task is complete or not) cannot be obtained directly like recursion, and can only be determined by the return value of the task function. That is, if the return value of the task function is function, it indicates that the current task is not completed. You need to continue calling the task function. Otherwise, the task is complete. WorkLoop uses this method to determine the completion status of individual tasks.
Before we get into the actual logic of executing a task in workLoop, let’s use an example to get to the heart of determining task completion status.
There is a task calculate, which increments currentResult by one until it reaches three. The calculate does not call itself until it reaches 3. Instead, it returns itself; once it reaches 3, it returns null. That’s how outsiders will know whether Calculate has done its job.
const result = 3
let currentResult = 0
function calculate() {
currentResult++
if (currentResult < result) {
return calculate
}
return null
}
Copy the code
Here are the tasks, and next we simulate scheduling to execute calculate. But should be based on time slice, in order to observe the effect, only use setInterval simulation for time slice to suspend the restore task mechanism (fairly rough simulation, need to understand that this is the simulation time, focus on the judgment of task completion status), 1 second to perform it, that is a only a third of the complete all tasks.
In addition, there are two queues in Scheduler to manage tasks, so we will use only one (taskQueue) to store tasks for the moment. Three other roles are needed: the function that adds tasks to the schedule (scheduling entry scheduleCallback), the function that starts the schedule (requestHostCallback), and the function that executes the task (workLoop, where the key logic resides).
const result = 3
let currentResult = 0
function calculate() {
currentResult++
if (currentResult < result) {
return calculate
}
return null
}
// Store the queue of tasks
const taskQueue = []
// Store the analog timer
let interval
// Scheduling entry ----------------------------------------
const scheduleCallback = (task, priority) = > {
// Create a task specific to the scheduler
const taskItem = {
callback: task,
priority
}
// Add tasks to the queue
taskQueue.push(taskItem)
// Priority affects the order of tasks in the queue, with the highest priority tasks placed first
taskQueue.sort((a, b) = > (a.priority - b.priority))
// Start task execution, scheduling starts
requestHostCallback(workLoop)
}
// Start scheduling -----------------------------------------
const requestHostCallback = cb= > {
interval = setInterval(cb, 1000)}// Execute the task -----------------------------------------
const workLoop = () = > {
// Fetch the task from the queue
const currentTask = taskQueue[0]
// Get the real task function, calculate
const taskCallback = currentTask.callback
// Determine whether the task function is a function, and if so, execute it, updating the return value to the callback of currentTask
// So, taskCallback is the return value of the previous execution, if it is a function type, then the last execution returned a function
// type, indicating that the task is not complete, continue this function, otherwise, indicating that the task is complete.
if (typeof taskCallback === 'function') {
currentTask.callback = taskCallback()
console.log('Executing task, current currentResult is', currentResult);
} else {
// Task completed. Remove the current task from the taskQueue and clear the timer
console.log('Task completed, final currentResult is', currentResult);
taskQueue.shift()
clearInterval(interval)
}
}
// Add Calculate to the schedule, which means the schedule begins
scheduleCallback(calculate, 1)
Copy the code
The final result is as follows:
CurrentResult: 1 Is executing, currentResult: 2 is executing, currentResult: 3 The task is complete, and the final currentResult is 3Copy the code
Calculate will return itself if it does not add to 3. WorkLoop will continue to call the task function to complete the task if it returns function.
This example only retains the logic in the workLoop to determine the completion status of the task. The rest is not perfect, but the real workLoop will be the standard. Now let’s post the entire code to see the real implementation in its entirety:
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// Check expired tasks in timerQueue before starting execution.
// To the taskQueue
advanceTimers(currentTime);
// Get the most urgent task in the taskQueue
currentTask = peek(taskQueue);
// Loop taskQueue to execute the task
while( currentTask ! = =null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if( currentTask.expirationTime > currentTime && (! hasTimeRemaining || shouldYieldToHost()) ) {// Time slice limit, interrupt task
break;
}
/ / a mission -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
// Get the task's execution function, the React callback to the Scheduler
// task. For example: performConcurrentWorkOnRoot
const callback = currentTask.callback;
if (typeof callback === 'function') {
// If the function is called function, there is something else to do
currentTask.callback = null;
// Get the priority of the task
currentPriorityLevel = currentTask.priorityLevel;
// Whether the task has expired
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// Get the execution result of the task function
const continuationCallback = callback(didUserCallbackTimeout);
if (typeof continuationCallback === 'function') {
// Check if the result of callback returns a function, and if it does, use this function as the new callback for the current task.
/ / concurrent mode, the callback is performConcurrentWorkOnRoot, its internal according to the current scheduled tasks
// If it is the same, it will return itself. If it is the same, it will return itself
// Is placed on the current task. After the while loop completes once, check shouldYieldToHost, if it needs to delegate execution,
CurrentTask is not null and returns true. PerformWorkUntilDeadline
PostMessage (null), call performWorkUntilDeadline (executor),
// Continue calling the workLoop line task
// Assign the returned value to currenttask.callback so that the next time the callback can continue,
// Get its return value and continue to determine whether the task is complete.
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
// Continue to fetch tasks from the taskQueue. If the previous task was not completed, it will not
// currentTask is removed from the queue, so the fetched currentTask is the same as the previous task and continues
// Execute it
currentTask = peek(taskQueue);
}
// The result of return will be performWorkUntilDeadline
// is the basis for determining whether to initiate scheduling again
if(currentTask ! = =null) {
return true;
} else {
// If the task is complete, go to timerQueue to find the task that needs to be executed earliest
// Schedule requestHostTimeout to wait until its start event is reached
// Put it in the taskQueue and schedule it again
const firstTimer = peek(timerQueue);
if(firstTimer ! = =null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false; }}Copy the code
Therefore, workLoop identifies the completion status of the task by judging the return value of the task function.
To summarize the overall relationship between task completion status and task execution: when scheduling starts, the scheduler dispatches the executor to execute the task, which is actually to execute the callback (i.e. task function) on the task. If the executor determines that the callback return value is a function, then the returned function will be assigned to the task’s callback, and the task will not be removed from the taskQueue because the task has not completed. CurrentTask will still retrieve the function. The while loop continues to execute the task until the next task is queued.
In addition there is a point need to mention, is build fiber tree task functions: performConcurrentWorkOnRoot, it accepts parameters is fiberRoot.
function performConcurrentWorkOnRoot(root) {... }Copy the code
It will be so call in workLoop (callback to performConcurrentWorkOnRoot) :
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
Copy the code
DidUserCallbackTimeout is clearly the value of the Boolean type is not fiberRoot, but performConcurrentWorkOnRoot normal calls. This is because root is passed in during bind at the beginning of the schedule, as well as the subsequent return itself.
// When you are scheduling
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
// its internal return itself
function performConcurrentWorkOnRoot(root) {...if (root.callbackNode === originalCallbackNode) {
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
Copy the code
So, give it pass parameters call it again, that this parameter is only as a subsequent arguments are received, performConcurrentWorkOnRoot received in the first parameter or bind when introduced to the root, the characteristics related to the realization of the bind. Here’s a simple example:
function test(root, b) {
console.log(root, b)
}
function runTest() {
return test.bind(null.'root')
}
runTest()(false)
// Result: root false
Copy the code
The above are the two core logic of Scheduler when executing tasks: interruption and recovery of tasks & judgment of task completion status. They work together, and if a task is interrupted by an unfinished achievement, the new performer of the schedule resumes the task until it completes. At this point, the core of the Scheduler is written, and here is the logic for unscheduling.
Cancel the schedule
The task execution is actually a callback of the task being executed. If the callback is function, what happens when the callback is null? The current task is removed from the taskQueue. Let’s look at the workLoop function again:
function workLoop(hasTimeRemaining, initialTime) {...// Get the most urgent task in the taskQueue
currentTask = peek(taskQueue);
while(currentTask ! = =null) {...const callback = currentTask.callback;
if (typeof callback === 'function') {
// Execute the task
} else {
// If callback is null, the task is dequeuedpop(taskQueue); } currentTask = peek(taskQueue); }... }Copy the code
So the key to unscheduling is to set the callback for the current task to NULL.
function unstable_cancelCallback(task) {... task.callback =null;
}
Copy the code
Why does setting callback to NULL cancel a task? In workLoop, if the callback is null, it will be removed from the taskQueue, so the current task will not be executed. It cancels the execution of the current task, and the while loop continues to execute the next task.
What is the React scenario for canceling missions? This is just one of many scenarios where an update task is in progress and a high-priority task suddenly comes in and cancels the ongoing task.
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {...if(existingCallbackNode ! = =null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
return;
}
// Cancel the original taskcancelCallback(existingCallbackNode); }... }Copy the code
conclusion
Scheduler uses task priority to realize multi-task management, prioritises high-priority tasks, and uses continuous task scheduling to solve the single task interruption recovery problem caused by time slice. The execution result of the task function provides reference for whether to end the current task scheduling. In addition, completing part of the task within a limited time slice also provides guarantee for the browser to respond to interaction and complete the task.