Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

What is the principle of React scheduling mechanism?

2025-01-27 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly introduces "what is the principle of the scheduling mechanism of React". In the daily operation, I believe that many people have doubts about the principle of the scheduling mechanism of React. The editor consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the doubts about "what is the principle of the scheduling mechanism of React?" Next, please follow the editor to study!

Click to enter the React source code debugging warehouse.

As an independent package, Scheduler can undertake the responsibility of task scheduling alone. You only need to give the task and its priority to it, and it can help you manage the task and arrange the execution of the task. This is how React and Scheduler work together.

For multiple tasks, it executes the high priority first. Focusing on the execution of a single task will be carried out in a controlled manner by Scheduler. In other words, there is only one thread, and it does not always occupy the thread to perform the task. But to execute for a while, to interrupt, and so on. Using this mode, we can avoid taking up limited resources to execute time-consuming tasks, solve the problem of page stutter during user operation, and achieve faster response.

We can sort out two important behaviors in Scheduler: the management of multiple tasks and the execution control of a single task.

Basic concept

In order to achieve the above two behaviors, it introduces two concepts: task priority and time slice.

Task priority allows tasks to be sorted according to their own urgency, so that the highest priority tasks are executed first.

The time slice specifies the maximum execution time of a single task in this frame. Once the execution time of the task exceeds the time slice, it will be interrupted and the task will be executed in a controlled manner. This ensures that the page will not stutter due to the continuous execution of the task for too long.

Overview of the principle

Based on the concept of task priority and time slice, Scheduler revolves around its core goal-task scheduling, and derives two core functions: task queue management and task interrupt and recovery under time slice.

Task queue management

Task queue management corresponds to the behavior of Scheduler's multitasking management. Within Scheduler, tasks are divided into two types: those that have not expired and those that have expired, which are stored in two queues, the former in timerQueue and the latter in taskQueue.

How can I tell whether a task is overdue?

Compare the start time of the task (startTime) with the current time (currentTime). If the start time is greater than the current time, it means it has not expired. If the start time of timerQueue; is less than or equal to the current time, it means that it has expired and put it on taskQueue.

How are tasks in different queues sorted?

When tasks join the queue one by one, it is natural to sort them to ensure that urgent tasks come first, so the ranking is based on the urgency of the tasks. However, the criteria for determining the degree of task urgency in taskQueue and timerQueue are different.

In taskQueue, tasks are sorted according to their expiration time (expirationTime). The earlier the expiration time is, the more urgent the task is, and the shorter the expiration time is, the more urgent the task is. The expiration time is calculated according to the task priority, and the higher the priority, the earlier the expiration time.

In timerQueue, according to the start time (startTime) of the task, the earlier the start time, the earlier the statement will start, and the smaller the start time will be. When a task comes in, the start time defaults to the current time. If a delay time is passed when entering the schedule, the start time is the sum of the current time and the delay time.

The mission joined two queues, and then what happened?

If you put it in taskQueue, immediately schedule a function to loop through taskQueue and execute the tasks in it one by one.

If you put it into the timerQueue, then all the tasks in it will not be executed immediately. Wait until the start time of the first task in the timerQueue to see whether the task expires. If so, take the task out of the timerQueue and put it into taskQueue, and schedule a function to loop it and execute the tasks inside. Otherwise, continue to check whether the first task expires later.

Compared with the execution of a single task, task queue management is a macro-level concept. It uses the priority of tasks to manage the order of tasks in the task queue, and always gives priority to the most urgent tasks.

Interruption and recovery of individual tasks

The interruption and recovery of a single task corresponds to the behavior of Scheduler's single task execution control. When a loop taskQueue executes each task, if a task takes too long to reach the time slice limit, the task must be interrupted in order to give way to something more important (such as browser drawing), and then resume execution of the task.

For example, click the button to render 140000 DOM nodes so that React can schedule a time-consuming update task through scheduler. Drag the box at the same time to simulate user interaction. The update task takes the thread to execute the task, and the user interaction also takes the thread to respond to the page, which determines that the two are mutually exclusive. In React's concurrent mode, when an update task scheduled through Scheduler encounters user interaction, it will look like this in the following diagram.

Executing React tasks and page response interactions are mutually exclusive, but because Scheduler can use time slices to interrupt React tasks and then let threads be sent to browsers to draw, dragging boxes will get timely feedback at first during the construction phase of the fiber tree. But it got stuck a little bit later, because the fiber tree was built and entered the synchronous commit phase, resulting in interaction stutters. The analysis of the rendering process of the page can be seen very visually through the control of time slices. The main thread is allowed to draw the page (Painting and Rendering, green and purple).

In order to achieve this scheduling effect, Scheduler needs two roles: the scheduler of the task and the executor of the task. The scheduler dispatches an executor, who loops the taskQueue and executes the tasks one by one. When a task is executed for a long time, the executor will interrupt the task execution according to the time slice, and then tell the scheduler: the task I am executing now has been interrupted, and some of it has not been completed, but now I have to give way to something more important. You can schedule another executor so that the task can be completed later (task recovery). As a result, the scheduler knows that the task is not finished and needs to continue, and it will schedule another executor to continue to complete the task.

Through the cooperation of the executor and the dispatcher, the interruption and recovery of the task can be realized.

Summary of principle

Scheduler manages both taskQueue and timerQueue queues. It periodically puts expired tasks in timerQueue into taskQueue, and then lets the scheduler inform the executor to loop taskQueue to execute each task. The executor controls the execution of each task, once the execution time of a task exceeds the time limit. It will be interrupted, and then the current executor will exit, and before exiting, the scheduler will be notified to schedule a new executor to continue to complete the task, and the new executor will still interrupt the task according to the time slot when executing the task, and then exit and repeat the process until the current task is completely completed, the task will be removed from the taskQueue. Every task in taskQueue is handled in this way, and all tasks are finally completed, which is the complete workflow of Scheduler.

There is a key point here, that is, how does the executor know whether the task has been completed or not? This is another topic, that is, to judge the completion status of the task. It will be highlighted when explaining the details of the executor's task.

The above is an overview of the principle of Scheduler, and the following is a detailed interpretation of the joint working mechanism of React and Scheduler. It involves the connection between React and Scheduler, scheduling entry, task priority, task expiration time, task interruption and recovery, judging the completion status of the task and so on.

Detailed process

Before we begin, let's take a look at a schematic diagram of a system made up of React and Scheduler.

The whole system is divided into three parts:

Where the task is generated: React

The translator of communication between React and Scheduler: SchedulerWithReactIntegration

Task scheduler: Scheduler

In React, let the construction task of the fiber tree enter the scheduling process through the following code:

ScheduleCallback (schedulerPriorityLevel, performConcurrentWorkOnRoot.bind (null, root),)

Tasks are really scheduled by the translator to Scheduler,Scheduler, so why do you need the role of a translator?

The connection between React and Scheduler

Scheduler helps React schedule various tasks, but they are essentially two completely uncoupled things, each with its own priority mechanism, so an intermediate role is needed to connect them.

In fact, such a file is provided in react-reconciler to do this kind of work, which is SchedulerWithReactIntegration.old (new) .js. It translates their priorities so that React and Scheduler can read each other. In addition, some functions in Scheduler are encapsulated for React to use.

In the important file ReactFiberWorkLoop.js that performs the React task, the content about Scheduler is imported from SchedulerWithReactIntegration.old (new) .js. 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'

SchedulerWithReactIntegration.old (new) .js provides two scheduling entry functions for React by encapsulating the contents of Scheduler: scheduleCallback and scheduleSyncCallback. The task enters the scheduling process through the scheduling entry function.

For example, the task of building a fiber tree is scheduled through scheduleCallback in concurrent mode and completed by scheduleSyncCallback in synchronous rendering mode.

/ / concurrentMode / / convert the priority of this update task to scheduling priority / / schedulerPriorityLevel to scheduling priority const schedulerPriorityLevel = lanePriorityToSchedulerPriority (newCallbackPriority,); / / concurrent mode scheduleCallback (schedulerPriorityLevel, performConcurrentWorkOnRoot.bind (null, root),); / / synchronous rendering mode scheduleSyncCallback (performSyncWorkOnRoot.bind (null, root),)

Both of them are actually encapsulation of scheduleCallback in Scheduler, but the priority passed in is different. The former passes the scheduling priority calculated by the updated lane, while the latter passes the highest priority. Another difference is that the former gives the task directly to Scheduler, while the latter first puts the task in the synchronization queue of SchedulerWithReactIntegration.old (new) .js, and then gives the function of executing the synchronization queue to Scheduler to schedule with the highest priority, which means that it will be an immediately expired task and will be executed immediately, which ensures that the task will be executed in the next event loop.

Function scheduleCallback (reactPriorityLevel: ReactPriorityLevel, callback: SchedulerCallback, options: SchedulerCallbackOptions | void | null,) {/ / translate the priority of react into the priority of Scheduler const priorityLevel = reactPriorityToSchedulerPriority (reactPriorityLevel); / / call the scheduleCallback of Scheduler and pass in the priority to schedule return Scheduler_scheduleCallback (priorityLevel, callback, options);} function scheduleSyncCallback (callback: SchedulerCallback) {if (syncQueue = = null) {syncQueue = [callback] / / scheduling refresh syncQueue function immediateQueueCallbackNode = Scheduler_scheduleCallback (Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl,);} else {syncQueue.push (callback);} return fakeCallbackNode;} with the highest priority

Priority in Scheduler

Speaking of priorities, let's take a look at Scheduler's own priority levels, which define the following levels of priority for tasks:

Export const NoPriority = 0; / / No priority export const ImmediatePriority = 1; / / priority for immediate execution, highest level export const UserBlockingPriority = 2; / / priority for user blocking level export const NormalPriority = 3; / / normal priority export const LowPriority = 4; / / lower priority export const IdlePriority = 5; / / lowest priority, indicating that the task can be idle

The role of task priority has 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 taskQueue.

/ / different task expiration intervals corresponding to different priorities var IMMEDIATE_PRIORITY_TIMEOUT =-1; var USER_BLOCKING_PRIORITY_TIMEOUT = 250; var NORMAL_PRIORITY_TIMEOUT = 5000; var LOW_PRIORITY_TIMEOUT = 10000; / / Never times out var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;. / / calculate the expiration time (content in the scheduleCallback function) 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 can be considered as the current time var expirationTime = startTime + timeout for the time being

As you can see, the expiration time is the task start time plus the timeout, and the timeout is calculated by the task priority.

A more comprehensive explanation of priorities in React is in this article I wrote: priorities in React

Dispatch entry-scheduleCallback

Through the above combing, we know that scheduleCallback in Scheduler is the key point at the beginning of the scheduling process. Before entering this scheduling entry, let's take a look at the form of tasks in Scheduler:

Var newTask = {id: taskIdCounter++, / / task function callback, / / task priority priorityLevel, / / task start time startTime, / / task expiration time expirationTime, / / sort in small top heap queue according to sortIndex:-1,}

Callback: the real task function, the focus, that is, the external task function, such as the task function for building a fiber tree: performConcurrentWorkOnRoot

PriorityLevel: task priority, participating in the calculation of task expiration time

StartTime: indicates the time when a task starts, affecting its ordering in timerQueue

ExpirationTime: indicates when a task expires, affecting its ordering in taskQueue

SortIndex: the basis for sorting in a small top heap queue. After distinguishing whether a task is expired or non-expired, sortIndex will be assigned to expirationTime or startTime, providing a sorting basis for two small top heap queues (taskQueue,timerQueue)

The real point is callback, as a task function, its execution result will affect the judgment of the completion status of the task. As we will talk about later, there is no need to pay attention to it for the time being. Now let's take a look at what scheduleCallback does: it is responsible for generating scheduled tasks, putting them into timerQueue or taskQueue based on whether the tasks are expired, and then triggering the scheduling behavior to get the tasks into scheduling. The complete code is as follows:

Function unstable_scheduleCallback (priorityLevel, callback, options) {/ / get the current time, which is based on var currentTime = getCurrentTime () to calculate the task start time, expiration time and determine whether the task expires; / / determine the task start time var startTime; / / try to get the delay from the options, that is, the deferred time if (typeof options = = 'object' & & options! = = null) {var delay = options.delay If (typeof delay = = 'number' & & delay > 0) {/ / if there is a delay, then the task start time is the current time plus delay startTime = currentTime + delay;} else {/ / without delay, the task start time is the current time, that is, the task needs to start immediately startTime = currentTime;}} else {startTime = currentTime } / / calculate timeout var timeout; switch (priorityLevel) {case ImmediatePriority: timeout = IMMEDIATE_PRIORITY_TIMEOUT; / /-1 break; case UserBlockingPriority: timeout = USER_BLOCKING_PRIORITY_TIMEOUT; / / 250break; 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 task expiration time, task start time + timeout / / if the priority of immediate execution (ImmediatePriority), / / its expiration time is startTime-1, which means that var expirationTime = startTime + timeout expires immediately / / create a scheduled task var newTask = {id: taskIdCounter++, / / Real task function, focus on callback, / / Task priority priorityLevel, / / start time of the task Indicates when the task can execute startTime, / / the expiration time of the task expirationTime, / / the sort in the small top heap queue according to sortIndex:-1,} / / the following if...else determines the meaning of each branch: / / if the task does not expire, put newTask into timerQueue, call requestHostTimeout, / / the purpose is to check whether the task expires at the point in time of the start time of the first task in timerQueue, / / immediately add the task to taskQueue if it expires, start scheduling / / if the task has expired, put newTask into taskQueue and call requestHostCallback / / start scheduling and execution of tasks in taskQueue if (startTime > currentTime) {/ / tasks have not expired, and the start time is used as the basis for timerQueue sorting newTask.sortIndex = startTime Push (timerQueue, newTask) If (peek (taskQueue) = = null & & newTask = peek (timerQueue)) {/ / if there are no tasks in taskQueue and the current task is the top one in timerQueue / / then you need to check whether there are any tasks in timerQueue that need to be placed in taskQueue. This step implements if (isHostTimeoutScheduled) {/ / because a requestHostTimeout is about to be scheduled by calling / / requestHostTimeout So if it has been scheduled before, cancel cancelHostTimeout () } else {isHostTimeoutScheduled = true;} / / call requestHostTimeout to transfer tasks and turn on scheduling requestHostTimeout (handleTimeout, startTime-currentTime);}} else {/ / tasks have expired, and taskQueue sorting is based on expiration time newTask.sortIndex = expirationTime; push (taskQueue, newTask) / / start the task and use flushWork to execute taskQueue if (! isHostCallbackScheduled & &! isPerformingWork) {isHostCallbackScheduled = true; requestHostCallback (flushWork);}} return newTask;}

The focus of this process is on whether the task expires or not.

For unexpired tasks, timerQueue is placed and arranged by start time, and then requestHostTimeout is called to wait a moment, wait until the start time of the earliest task in timerQueue (the first task), and then check whether it expires, and if it expires, put it in taskQueue, so that the task can be executed, otherwise continue to wait. This process is done through handleTimeout.

The responsibilities of handleTimeout are:

Call advanceTimers to check for expired tasks in the timerQueue queue and put them in taskQueue.

Check whether scheduling has started, and if not, check whether there are already tasks in taskQueue:

If there is, and it is free now, it means that the previous advanceTimers has put the expired task to taskQueue, then start scheduling and executing the task immediately.

If not, and it is idle now, it means that the previous advanceTimers did not detect an expired task in timerQueue, then call requestHostTimeout again to repeat the process.

In short, you need to transfer all the tasks in timerQueue to taskQueue for execution.

For an expired task, after putting it into the taskQueue, call requestHostCallback and ask the scheduler to schedule an executor to execute the task, which means that the scheduling process begins.

Start scheduling-find dispatchers and executors

Scheduler allows the task to enter the scheduling process by calling requestHostCallback, and reviews where scheduleCallback finally calls requestHostCallback to execute the task:

If (! isHostCallbackScheduled & &! isPerformingWork) {isHostCallbackScheduled = true; / / start scheduling requestHostCallback (flushWork);}

Since it takes flushWork as the input parameter, the executor of the task essentially calls flushWork. No matter how the executor executes the task, we first pay attention to how it is scheduled. We need to find out the scheduler first. We need to take a look at the implementation of requestHostCallback:

Scheduler distinguishes browser environment from non-browser environment and makes two different implementations for requestHostCallback. In a non-browser environment, it is implemented using setTimeout.

RequestHostCallback = function (cb) {if (_ callback! = = null) {setTimeout (requestHostCallback, 0, cb);} else {_ callback = cb; setTimeout (_ flushCallback, 0);}}

In the browser environment, implemented in MessageChannel, the introduction to MessageChannel will not be repeated.

Const channel = new MessageChannel (); const port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; requestHostCallback = function (callback) {scheduledHostCallback = callback; if (! isMessageLoopRunning) {isMessageLoopRunning = true; port.postMessage (null);}}

The reason for the two implementations is that there is no screen refresh rate in the non-browser environment, and there will be no time slice without the concept of frame, which is essentially different from executing the task in the browser environment. Because there is basically no user interaction in the non-browser environment, it is not judged whether the task execution time exceeds the time slice limit in this scenario, while the browser environment task execution will have a time slice limit. Apart from this, although the two environments are implemented differently, they do roughly the same thing.

First look at the non-browser environment, which stores the input parameters (the function that executes the task) in the internal variable _ callback, and then dispatches _ flushCallback to execute this variable _ callback,taskQueue is cleared.

Look at the browser environment, which stores the input parameters (the function that executes the task) into the internal variable scheduledHostCallback, and then sends a message through MessageChannel's port to allow channel.port1 's listener function performWorkUntilDeadline to be executed. The scheduledHostCallback is executed inside the performWorkUntilDeadline, and finally the taskQueue is emptied.

From the above description, the scheduler can be clearly identified: the non-browser environment is setTimeout, and the browser environment is port.postMessage. The executor of the two environments is also obvious, the former is _ flushCallback, the latter is performWorkUntilDeadline, what the executor does is to call the actual task execution function.

Because this paper focuses on the time slice scheduling behavior of Scheduler, it mainly discusses the scheduling behavior in the browser environment. PerformWorkUntilDeadline involves calling the task execution function to execute the task, which will involve the interruption and recovery of the task and the judgment of the completion state of the task. The following content will focus on these two points.

Mission execution-start with performWorkUntilDeadline

In the overview of the principles at the beginning of the article, it is mentioned that performWorkUntilDeadline 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 to continue the task. According to this understanding, it will be very clear to see its realization.

Const performWorkUntilDeadline = () = > {if (scheduledHostCallback! = = null) {/ / get the current time const currentTime = getCurrentTime (); / / calculate that deadline,deadline will participate in / / shouldYieldToHost (restrict task execution according to time slices) deadline = currentTime + yieldInterval / / hasTimeRemaining indicates whether the task has time left. / / it, together with the time slice, restricts the execution of the task. If there is no time, / or the execution time of the task exceeds the time slice limit, then the task is interrupted. / / it defaults to true, indicating that there is always time left / / because MessageChannel's port is in postMessage, / / it is a macro task executed earlier than setTimeout, which means / / at the beginning of this frame, there will always be time left / / so now interrupting the task only looks at the time slice const hasTimeRemaining = true Try {/ / scheduledHostCallback function to execute the task, / / when the task is interrupted because of a time slice, it will return true, indicating / / there is still a task, so it will ask the scheduler to schedule an executor / / to continue to execute the task const hasMoreWork = scheduledHostCallback (hasTimeRemaining, currentTime,) If (! hasMoreWork) {/ / if there are no tasks, stop scheduling isMessageLoopRunning = false; scheduledHostCallback = null;} else {/ / if there are any tasks, continue to let the dispatcher schedule the executor to facilitate the continuation / / completion of the task port.postMessage (null) }} catch (error) {port.postMessage (null); throw error;}} else {isMessageLoopRunning = false;} needsPaint = false;}

PerformWorkUntilDeadline calls scheduledHostCallback internally, which is assigned to flushWork by requestHostCallback as early as the start of scheduling. Please refer to the above to review the implementation of requestHostCallback.

As a function that actually executes a task, flushWork loops through taskQueue, calling each of the task functions one by one. Let's take a look at exactly what flushWork did.

Function flushWork (hasTimeRemaining, initialTime) {... Return workLoop (hasTimeRemaining, initialTime);...}

It calls workLoop and return the result of its call. So now the core of task execution seems to be in workLoop. The call to workLoop causes the task to be executed eventually.

Task interruption and recovery

To understand workLoop, you need to review one of the functions of Scheduler: limiting the execution time of tasks through time slices. So since the execution of the task is restricted, it must be unfinished, and if the unfinished task is interrupted, it needs to be restored.

Therefore, the execution of tasks under the time slice has the following important features: it will be interrupted and resumed.

It is not difficult to speculate that as a function of actually executing a task, what workLoop does must have something to do with the interrupt recovery of the task. Let's take a look at its structure first:

Function workLoop (hasTimeRemaining, initialTime) {/ / get the first task in taskQueue currentTask = peek (taskQueue) While (currentTask! = = null) {if (currentTask.expirationTime > currentTime & (! hasTimeRemaining | | shouldYieldToHost () {/ / break remove while loop break}. / / execute task. / / after task execution is complete, delete pop (taskQueue) from the queue. / / get the next task and continue to cycle currentTask = peek (taskQueue);} if (currentTask! = = null) {/ / if the currentTask is not empty, it is the time slice limit that causes the task to be interrupted / / return A true tells the outside that the task is not finished yet, and there are still tasks, / / translated into English as hasMoreWork return true } else {/ / if the currentTask is empty, all the tasks in the taskQueue queue have been / / executed, then find the task from the timerQueue, call requestHostTimeout / / to put the task into the taskQueue, and the schedule will be initiated again, but this time, / / will return false first, telling the external current taskQueue has been emptied, / / stop executing the task first That is, to terminate the task scheduling const firstTimer = peek (timerQueue) If (firstTimer! = = null) {requestHostTimeout (handleTimeout, firstTimer.startTime-currentTime);} return false;}}

WorkLoop can be divided into two parts: cyclic taskQueue execution task and task status judgment.

Loop taskQueue to perform tasks

Regardless of how the task is executed, just focus on how the task is limited by the time slice, in workLoop:

If (currentTask.expirationTime > currentTime & & (! hasTimeRemaining | | shouldYieldToHost ()) {/ / break minus while loop break}

CurrentTask is the task currently being executed. The judgment condition for its termination is that the task has not expired, but there is no time left (because hasTimeRemaining has always been true, which is related to the timing of execution of MessageChannel as a macro task, we ignore this judgment condition and only look at time slices), or we should cede execution power to the main thread (time slice limit), that is to say, currentTask executes well, but time does not allow. You can only break this while loop so that none of the logic executed by currentTask under this loop can be executed (here is the key to interrupting the task). But what is break is only the while loop, and the lower part of the while will still judge the state of the currentTask.

Since it is only aborted, currentTask cannot be a null, so a true will tell the outside world that it is not finished yet (here is the key to the recovery task). Otherwise, it means that all the tasks have been executed and the taskQueue has been emptied. Return a false so that the external can terminate the scheduling. The execution result of workLoop will be outputted by flushWork return, and flushWork is actually scheduledHostCallback. When performWorkUntilDeadline detects that the return value (hasMoreWork) of scheduledHostCallback is false, it will stop scheduling.

Reviewing the behavior in performWorkUntilDeadline, you can clearly concatenate the mechanism for task interruption recovery:

Const performWorkUntilDeadline = () = > {. Const hasTimeRemaining = true; / / scheduledHostCallback function to execute the task, / / when the task is interrupted because of a time slice, it will return true, indicating / / there is still a task, so it will ask the scheduler to schedule another executor / / to continue to execute the task const hasMoreWork = scheduledHostCallback (hasTimeRemaining, currentTime,) If (! hasMoreWork) {/ / if there are no tasks, stop scheduling isMessageLoopRunning = false; scheduledHostCallback = null;} else {/ / if there are any tasks, continue to let the dispatcher schedule the executor to facilitate the continuation / completion of the task port.postMessage (null);}}

When the task is interrupted, performWorkUntilDeadline asks the scheduler to call an executor to continue the task until the task is complete. But there is a key point here is how to determine whether the task is completed? This requires a study of the part of the logic that performs tasks in workLoop.

Determine the completion status of a single task

The interruption recovery of a task is a repetitive process that repeats until the task is completed. Therefore, it is very important to determine whether the task is completed, and if the task is not completed, the task function will be executed repeatedly.

We can use a recursive function as an analogy and call ourselves repeatedly if we don't reach the recursive boundary. This recursive boundary is the sign that the task is completed. Because the task handled by the recursive function is itself, it is convenient to finish the task as a recursive boundary, but the workLoop in Scheduler is different from recursion in that it only executes the task, which is not generated by itself, but external (for example, it executes the work cycle of React to render the fiber tree). It can repeat the task function. However, the boundary (that is, whether the task is completed or not) can not be obtained directly like recursion, and can only be judged by the return value of the task function. That is, if the return value of the task function is a function, it means that the current task has not been completed, and you need to continue to call the task function, otherwise the task is completed. WorkLoop uses this method to determine the completion status of a single task.

Before we really explain the logic of task execution in workLoop, let's use an example to understand the core of determining the state of task completion.

There is a task, calculate, which is responsible for adding 1 at a time to currentResult until 3. When it is less than 3, calculate does not call itself, but return itself out. Once it gets to 3, null returns. In this way, the outside world can know whether the calculate has completed the task.

Const result = 3 let currentResult = 0 function calculate () {currentResult++ if (currentResult

< result) { return calculate } return null } 上面是任务,接下来我们模拟一下调度,去执行calculate。但执行应该是基于时间片的,为了观察效果,只用setInterval去模拟因为时间片中止恢复任务的机制(相当粗糙的模拟,只需明白这是时间片的模拟即可,重点关注任务完成状态的判断),1秒执行它一次,即一次只完成全部任务的三分之一。 另外Scheduler中有两个队列去管理任务,我们暂且只用一个队列(taskQueue)存储任务。除此之外还需要三个角色:把任务加入调度的函数(调度入口scheduleCallback)、开始调度的函数(requestHostCallback)、执行任务的函数(workLoop,关键逻辑所在)。 const result = 3 let currentResult = 0 function calculate() { currentResult++ if (currentResult < result) { return calculate } return null } // 存放任务的队列 const taskQueue = [] // 存放模拟时间片的定时器 let interval // 调度入口---------------------------------------- const scheduleCallback = (task, priority) =>

{/ / create a task dedicated to the scheduler const taskItem = {callback: task, priority} / / add a task to the queue taskQueue.push (taskItem) / / priority affects the sorting of tasks in the queue Put the highest priority task first taskQueue.sort ((a, b) = > (a.priority-b.priority)) / / start to execute the task Scheduling start requestHostCallback (workLoop)} / / start scheduling-- const requestHostCallback = cb = > {interval = setInterval (cb) 1000)} / / execute the task-- const workLoop = () = > {/ / take the task from the queue const currentTask = taskQueue [0] / / get the real task function That is, calculate const taskCallback = currentTask.callback / / determines whether the task function is a function. If it is executed, the return value will be updated to the callback of currentTask / / so, taskCallback is the return value of the previous phase. If it is a function type, the function / / type was returned in the last execution, indicating that the task has not been completed, and the function is continued to be executed this time, otherwise the task is completed. If (typeof taskCallback = = 'function') {currentTask.callback = taskCallback () console.log (' in progress, the current currentResult is', currentResult);} else {/ / task complete. Remove the current task from the taskQueue and clear the timer console.log ('task completed, the final currentResult is', currentResult); taskQueue.shift () clearInterval (interval)}} / / add calculate to the schedule, which means that the schedule starts scheduleCallback (calculate, 1)

The final implementation result is as follows:

The task is being executed, the current currentResult is 1, the current currentResult is 2, the current currentResult is 3, and the final currentResult is 3

It can be seen that if it is not added to 3, then calculate will return itself, and if workLoop determines that the return value is function, indicating that the task has not been completed, it will continue to call the task function to complete the task.

This example only retains the logic of judging the completion status of a task in workLoop, and the rest is not perfect. Let's post all its code and take a complete look at the real implementation, depending on the real workLoop:

Function workLoop (hasTimeRemaining, initialTime) {let currentTime = initialTime; / / check the expired tasks in timerQueue before starting execution, / / put advanceTimers (currentTime) in taskQueue; / / get the most urgent task in taskQueue currentTask = peek (taskQueue) / / cycle taskQueue, execute task while (currentTask! = = null & &! (enableSchedulerDebugging & & isSchedulerPaused)) {if (currentTask.expirationTime > currentTime & & (! hasTimeRemaining | | shouldYieldToHost () {/ / interrupt task break } / / execute the task-- / / get the execution function of the task. This callback is the task that React passes to Scheduler / /. For example: performConcurrentWorkOnRoot const callback = currentTask.callback; if (typeof callback = = 'function') {/ / if the execution function is function, it means there are still tasks to do. Call it currentTask.callback = null; / / to get the priority of the task currentPriorityLevel = currentTask.priorityLevel; / / whether the task expires const didUserCallbackTimeout = currentTask.expirationTime

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report