react source code analysis 15 scheduler&Lane
Video Explanation (efficient learning): Enter learning
When we search in the search box component like the following, we will find that the component is divided into search part and search result display list. We expect the input box to respond immediately, and the result list can have waiting time. If the data in the result list is large, we enter some text during rendering, because the priority of user input events is very high, Therefore, it is necessary to stop the rendering of the result list, which leads to the priority and scheduling between different tasks
Scheduler
We know that if our application takes a long js execution time, such as more than one frame of the device, the rendering of the device will be abnormal.
The Scheduler's main functions are time slicing and scheduling priority. React will occupy a certain js execution time when comparing differences. The Scheduler uses MessageChannel to specify a time slice before drawing by the browser. If react is not compared within the specified time, the Scheduler will forcibly hand over the execution right to the browser
Time slice
The execution time of js in one frame of the browser is as follows
requestIdleCallback can be executed when the browser is free after redrawing and rearrangement. Therefore, in order not to affect the redrawing and rearrangement, the browser can perform performance consumption calculation in requestIdleCallback. However, due to the problems of compatibility and unstable trigger time of requestIdleCallback, MessageChannel is used in the scheduler to implement requestIdleCallback, The current environment does not support MessageChannel, so setTimeout is adopted.
In the previous introduction, we know that the render phase and the commit phase will be executed after performing unitofwork. If the calculation of the cup is not completed in a frame of the browser, the js execution right will be given to the browser. In the workLoopConcurrent function, shouldYield is used to judge whether the remaining time is exhausted. In the source code, each time slice is 5ms, and this value will be adjusted according to the fps of the device.
function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
function forceFrameRate(fps) {//Calculate time slice if (fps < 0 || fps > 125) { console['error']( 'forceFrameRate takes a positive int between 0 and 125, ' + 'forcing frame rates higher than 125 fps is not supported', ); return; } if (fps > 0) { yieldInterval = Math.floor(1000 / fps); } else { yieldInterval = 5;//The default time slice is 5ms } }
Suspension of tasks
There is a period in the shouldYield function, so you can know that if the current time is greater than the task start time + yield interval, the task will be interrupted.
//deadline = currentTime + yieldInterval. deadline is calculated in performWorkUntilDeadline function if (currentTime >= deadline) { //... return true }
Scheduling priority
There are two functions in the Scheduler to create tasks with priority
-
runWithPriority: execute callback with a priority. If it is a synchronous task, the priority is ImmediateSchedulerPriority
function unstable_runWithPriority(priorityLevel, eventHandler) { switch (priorityLevel) {//5 priorities case ImmediatePriority: case UserBlockingPriority: case NormalPriority: case LowPriority: case IdlePriority: break; default: priorityLevel = NormalPriority; } var previousPriorityLevel = currentPriorityLevel;//Save current priority currentPriorityLevel = priorityLevel;//priorityLevel is assigned to currentPriorityLevel try { return eventHandler();//Callback function } finally { currentPriorityLevel = previousPriorityLevel;//Priority before restore } }
-
scheduleCallback: register the callback with a priority and execute it at an appropriate time. Because it involves the calculation of expiration time, the granularity of scheduleCallback is finer than that of runWithPriority.
-
In scheduleCallback, priority means expiration time. The higher the priority, the smaller the priorityLevel, and the closer the expiration time is to the current time. var expirationTime = startTime + timeout; For example, IMMEDIATE_PRIORITY_TIMEOUT=-1, var expirationTime = startTime + (-1); It is less than the current time, so it should be executed immediately.
-
The scheduleCallback scheduling process uses a small top heap, so we can find the task with the highest priority in the complexity of O(1). If we don't know, we can consult the data. There are tasks in the small top heap of the source code, and each peek can get the task closest to the expiration time.
-
In scheduleCallback, unexpired tasks are stored in timerQueue, and expired tasks are stored in taskQueue.
After a new newTask task is created, judge whether the newTask has expired. If it has not expired, join the timerQueue. If there is no expired task in the taskQueue at this time, and the task closest to the expiration time in the timerQueue is just a newTask, set a timer and join the taskQueue at the expiration time.
When there are tasks in the timerQueue, the earliest expired tasks are retrieved for execution.
-
function unstable_scheduleCallback(priorityLevel, callback, options) { var currentTime = getCurrentTime(); var startTime;//start time if (typeof options === 'object' && options !== null) { var delay = options.delay; if (typeof delay === 'number' && delay > 0) { startTime = currentTime + delay; } else { startTime = currentTime; } } else { startTime = currentTime; } var timeout; switch (priorityLevel) { case ImmediatePriority://The higher the priority, the smaller the timeout timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1 break; case UserBlockingPriority: timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250 break; case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break; case LowPriority: timeout = LOW_PRIORITY_TIMEOUT; break; case NormalPriority: default: timeout = NORMAL_PRIORITY_TIMEOUT; break; } var expirationTime = startTime + timeout;//The higher the priority, the smaller the expiration time var newTask = {//New task id: taskIdCounter++, callback//Callback function priorityLevel, startTime,//start time expirationTime,//Expiration time sortIndex: -1, }; if (enableProfiling) { newTask.isQueued = false; } if (startTime > currentTime) {//Not expired newTask.sortIndex = startTime; push(timerQueue, newTask);//Join timerQueue //There are no expired tasks in the taskQueue. The task closest to the expiration time in the timerQueue happens to be a newTask if (peek(taskQueue) === null && newTask === peek(timerQueue)) { if (isHostTimeoutScheduled) { cancelHostTimeout(); } else { isHostTimeoutScheduled = true; } //Timer. When the expiration time is reached, it will be added to taskQueue requestHostTimeout(handleTimeout, startTime - currentTime); } } else { newTask.sortIndex = expirationTime; push(taskQueue, newTask);//Join taskQueue if (enableProfiling) { markTaskStart(newTask, currentTime); newTask.isQueued = true; } if (!isHostCallbackScheduled && !isPerformingWork) { isHostCallbackScheduled = true; requestHostCallback(flushWork);//Perform overdue tasks } } return newTask; }
How to continue after the task is suspended
There is such a paragraph in the workLoop function
const continuationCallback = callback(didUserCallbackTimeout);//A callback is a scheduled callback currentTime = getCurrentTime(); if (typeof continuationCallback === 'function') {//Judge the return value type after callback execution currentTask.callback = continuationCallback;//If it is a function type, it will be assigned to currenttask callback markTaskYield(currentTask, currentTime); } else { if (enableProfiling) { markTaskCompleted(currentTask, currentTime); currentTask.isQueued = false; } if (currentTask === peek(taskQueue)) { pop(taskQueue);//If it is a function type, it is deleted from taskQueue } } advanceTimers(currentTime);
At the end of the performcurrentworkonroot function, there is a judgment that if the callbackNode is equal to the originalCallbackNode, the execution of the task will be resumed
if (root.callbackNode === originalCallbackNode) { // The task node scheduled for this root is the same one that's // currently executed. Need to return a continuation. return performConcurrentWorkOnRoot.bind(null, root); }
Lane
Lane and Scheduler are two sets of priority mechanisms. Compared with lane, lane has finer priority granularity. Lane means lane. Like a racing car, when a task obtains priority, it always gives priority to the track in the inner circle. The priority represented by Lane has the following characteristics.
-
It can represent the priority of different batches
It can be seen from the code that each priority is a 31 bit binary number. 1 means that the position can be used, and 0 means that the position cannot be used. From the first priority NoLanes to offscreen lane, the priority is reduced. The lower the priority, the more the number of 1 (the more cars in the outer ring of the race), that is, the priority with multiple 1 is the same batch.
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000; export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010; export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100; const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000; const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000; const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000; export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000; export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000; const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000; const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000; const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000; export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000; export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000; const NonIdleLanes = /* */ 0b0000111111111111111111111111111; export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000; export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
-
High performance of priority calculation
For example, it is possible to judge whether there is an intersection between the lane s represented by a and b by binary bitwise AND
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) { return (a & b) !== NoLanes; }
How do task s in the Lane model get priority (the initial track of the car)
The task obtains the track from the high priority lanes. This process occurs in the findUpdateLane function. If there are no available lanes in the high priority, it will drop to the low priority lanes to find them. Pickarbitrrylane will call getHighestPriorityLane to obtain the highest priority in a batch of lanes, That is, the rightmost bit is obtained through lanes & - lanes
export function findUpdateLane( lanePriority: LanePriority, wipLanes: Lanes, ): Lane { switch (lanePriority) { //... case DefaultLanePriority: { let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);//Find the next lane with the highest priority if (lane === NoLane) {//The lanes of the last level are full. Drop to transition lanes and continue to look for available tracks lane = pickArbitraryLane(TransitionLanes & ~wipLanes); if (lane === NoLane) {//Transition lanes are full, too lane = pickArbitraryLane(DefaultLanes);//Start with DefaultLanes } } return lane; } } }
How does the high priority in the Lane model jump in the queue (racing to grab the track)
In the Lane model, if a low priority task is executed and a high priority task is triggered during scheduling, the high priority task interrupts the low priority task. At this time, the low priority task should be cancelled first, because the low priority task may have been in progress for some time and part of the Fiber tree has been built, Therefore, you need to restore the Fiber tree. This process occurs in the function prepareFreshStack, which initializes the built Fiber tree
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { const existingCallbackNode = root.callbackNode;//Callback of setState that has been called before //... if (existingCallbackNode !== null) { const existingCallbackPriority = root.callbackPriority; //If the callback priority of the new setState is equal to that of the previous setState, it will enter the logic of batchedUpdate if (existingCallbackPriority === newCallbackPriority) { return; } //If the two callback priorities are inconsistent, they will be interrupted by the high priority task. First cancel the current low priority task cancelCallback(existingCallbackNode); } //Starting point of scheduling render phase newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), ); //... }
function prepareFreshStack(root: FiberRoot, lanes: Lanes) { root.finishedWork = null; root.finishedLanes = NoLanes; //... //Reassign and initialize variables such as workInProgressRoot workInProgressRoot = root; workInProgress = createWorkInProgress(root.current, null); workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; workInProgressRootExitStatus = RootIncomplete; workInProgressRootFatalError = null; workInProgressRootSkippedLanes = NoLanes; workInProgressRootUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; //... }
How to solve the hunger problem in the Lane model (the last car has to reach the finish line)
In the process of scheduling priority, markstarvedlanes asexpired will be called to traverse pendingLanes (the lane contained in the unexecuted task). If there is no expiration time, an expiration time will be calculated. If it is expired, it will be added to root.expiredLanes, and then expiredLanes will be returned first when getNextLane function is called next time
export function markStarvedLanesAsExpired( root: FiberRoot, currentTime: number, ): void { const pendingLanes = root.pendingLanes; const suspendedLanes = root.suspendedLanes; const pingedLanes = root.pingedLanes; const expirationTimes = root.expirationTimes; let lanes = pendingLanes; while (lanes > 0) {//Traversal lanes const index = pickArbitraryLaneIndex(lanes); const lane = 1 << index; const expirationTime = expirationTimes[index]; if (expirationTime === NoTimestamp) { if ( (lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes ) { expirationTimes[index] = computeExpirationTime(lane, currentTime);//Calculate expiration time } } else if (expirationTime <= currentTime) {//Out of date root.expiredLanes |= lane;//Add the currently traversed lane in expiredLanes } lanes &= ~lane; } }
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { //... if (expiredLanes !== NoLanes) { nextLanes = expiredLanes; nextLanePriority = return_highestLanePriority = SyncLanePriority;//Return expired lane first } else { //... } return nextLanes; }
The following figure is more intuitive. With the passage of time, low priority tasks will be cut in the queue and eventually become high priority tasks