Welcome to read the React source code analysis series:
React16 source code analysis (I) - illustration of Fiber architecture
React16 source code analysis (2) - create update
React16 source code analysis (III) - ExpirationTime
React16 source code analysis (IV) - Scheduler
React16 source code analysis (V) - update process rendering stage 1
React16 source code analysis (VI) - update process rendering stage 2
React16 source code analysis (7) - update process rendering stage 3
React16 source code analysis (VIII) - update process submission stage
Updating...
At the end of react 16 source code parsing (2) - create update, all three types of updates call scheduleWork to enter task scheduling.
// current is RootFiber scheduleWork(current, expirationTime)
design idea
1. React arranges all tasks according to the expiration time from small to large, and the data structure adopts two-way circular list.
2. Implementation criteria of task list: the current frame first performs tasks such as browser rendering. If the current frame still has free time, the task will be executed until the current frame runs out of time. If there is no idle time in the current frame, wait until the next frame is idle. Note that if there is no idle time in the current frame, but there are tasks in the current task list that have expired or are executed immediately, the tasks must be executed at the cost of losing several frames. All completed tasks will be removed from the list.
Core functions
1. Maintenance time slice
2. Simulation browser requestdlecallback API
3. Scheduling list and timeout judgment
Basic knowledge
Basic knowledge for reading this article:
1,window.requestAnimationFrame
2,window.MessageChannel
3. List operation
No children's shoes can be understood first. I will not introduce them in detail here.
Get to the point and start reading
Let's start with the previous scheduleWork.
global variable
A large number of global variables are used in this. I will list them here. You can view them here in the following explanation:
isWorking: commitRoot and renderRoot are set to true at the beginning, and then reset to false at the end of their respective phases. It is used to mark whether an update is currently in progress, regardless of the stage. nextRoot: used to record the next root node to be rendered nextRenderExpirationTime: ExpirationTime of the next task to render First scheduledroot & last scheduledroot: a single list structure for all roots with tasks. It is used to retrieve the root with the highest priority in findHighestPriorityRoot, which will be modified in addRootToSchedule. callbackExpirationTime & callbackID: callbackExpirationTime records the expiration time used to request the ReactScheduler. If a new scheduling request comes in during a scheduling and has a higher priority, the last request needs to be cancelled. If it is lower, the scheduling need not be requested again. The callbackID is the ID returned by the react scheduler to cancel the schedule. Nextflushedroot & nextflushedexpirationtime: used to mark the next root to be rendered and the corresponding xpirationtime. Note: find the highest priority through findHighestPriorityRoot, and set the specified one directly through flushRoot without filtering.
scheduleWork (scheduled task)
After we update the update queue of fiber, we call schedulework to start scheduling this work. The main thing for scheduleWork is to find the root priority that we have to deal with, and then call requestWork.
1. Find and update the corresponding FiberRoot node (scheduleWorkToRoot) and return through the fiber.return layer by layer according to the tree structure until the root node is found. In the process of looking up, the childExpirationTime of the corresponding fiber object of each node is constantly updated. And alternate synchronizes updates.
Note: the highest priority expirationTime in the child expirationTime subtree.
2. If there is a previous task and the previous execution is not completed, the execution right is given to the browser. If the priority of the current update is higher than that of the previous task, reset the stack (resetStack).
Note: resetStack will recover step by step from next unitofwork. It can be said that half of the previous task has been executed in vain ~ because there are higher priority tasks to jump in the queue now! You say you are angry, but the world is so cruel.
3. After the above 2 conditions are met, if it is not in the render stage, or nextroot! = = root, the task enjoying vip treatment can request scheduling: requestWork.
Note: if we are in the render phase, we do not need to request scheduling, because the render phase will process the update.
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { // Get FiberRoot const root = scheduleWorkToRoot(fiber, expirationTime); if (root === null) { return; } // This branch indicates that the high priority task interrupts the low priority task. // This happens in the following scenario: a lower priority task (which must be asynchronous) is not completed. // The execution right is given to the browser. At this time, a new high priority task comes in. // At this time, you need to perform high priority tasks, so you need to interrupt low priority tasks. if ( !isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime ) { // Record who interrupted interruptedBy = fiber; // Reset stack resetStack(); } // ...... if ( // If we're in the render phase, we don't need to schedule this root // for an update, because we'll do it before we exit... !isWorking || isCommitting || // ...unless this is a different root than the one we're rendering. nextRoot !== root ) { const rootExpirationTime = root.expirationTime; // Request task requestWork(root, rootExpirationTime); } // setState can cause infinite loops in some life cycle functions // Here's to tell you that your code triggers an infinite loop if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { // Reset this back to zero so subsequent updates don't throw. nestedUpdateCount = 0; invariant( false, 'Maximum update depth exceeded. This can happen when a ' + 'component repeatedly calls setState inside ' + 'componentWillUpdate or componentDidUpdate. React limits ' + 'the number of nested updates to prevent infinite loops.', ); } }
requestWork (request task)
1. Add root to Schedule (addRootToSchedule). If this root has been scheduled (already in the one-way linked list of scheduledRoot), it may update root.expirationTime.
It maintains a one-way linked list of scheduleroot, for example, lastScheduleRoot == null, which means that we currently have no root to process. In this case, we set firstScheduleRoot, lastScheduleRoot and root.nextScheduleRoot to root. If lastScheduleRoot! = = null, set lastScheduledRoot.nextScheduledRoot to root. After the lastScheduledRoot is scheduled, the current root will be processed.
2. Is it a synchronization task? Yes: performSyncWork No: scheduleCallbackWithExpirationTime
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { // Add Root to Schedule and update root.expirationTime addRootToSchedule(root, expirationTime); if (isRendering) { // Prevent reentrancy. Remaining work will be scheduled at the end of // the currently rendering batch. return; } // Judge whether batch update is needed // When we trigger the event callback, the callback will be encapsulated by the batchedUpdates function once. // This function will set isBatchingUpdates to true, which means that we are in the event callback function. // Calling setState will not immediately trigger the update and rendering of state, but simply create an updater, and return in this branch. // The value of isBatchingUpdates is restored and performSyncWork is executed only after the whole event callback function is executed. // I think many people know that after using setState in setTimeout, the state will be updated immediately. If you want to implement batch update in timer callback, // You can use batched updates to encapsulate the code you need if (isBatchingUpdates) { // Flush work at the end of the batch. // Determine whether batch update is not required if (isUnbatchingUpdates) { // ...unless we're inside unbatchedUpdates, in which case we should // flush it now. nextFlushedRoot = root; nextFlushedExpirationTime = Sync; performWorkOnRoot(root, Sync, true); } return; } // TODO: Get rid of Sync and use current time? // Determine whether the priority is synchronous or asynchronous, which requires scheduling if (expirationTime === Sync) { performSyncWork(); } else { // The core function is to implement the polyfill version of requestIdleCallback. // Because of the poor compatibility of the function browser // For specific functions, see the MDN document https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback // This function allows the browser to call functions in turn during idle time, which enables developers to perform background or low priority tasks in the main event loop. // And it doesn't affect latency sensitive events like animation and user interaction. scheduleCallbackWithExpirationTime(root, expirationTime); } } function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) { // Add the root to the schedule. // Check if this root is already part of the schedule. // Determine whether root has been scheduled if (root.nextScheduledRoot === null) { // This root is not already scheduled. Add it. // root has not been scheduled root.expirationTime = expirationTime; if (lastScheduledRoot === null) { firstScheduledRoot = lastScheduledRoot = root; root.nextScheduledRoot = root; } else { lastScheduledRoot.nextScheduledRoot = root; lastScheduledRoot = root; lastScheduledRoot.nextScheduledRoot = firstScheduledRoot; } } else { // This root is already scheduled, but its priority may have increased. // root has been scheduled, judge whether the priority needs to be updated const remainingExpirationTime = root.expirationTime; if ( remainingExpirationTime === NoWork || expirationTime < remainingExpirationTime ) { // Update the priority. root.expirationTime = expirationTime; } } }
scheduleCallbackWithExpirationTime
1. If a callback has already been scheduled (callback expiration time! = = nowork), and the priority is higher than the current callback (expiration time > callback expiration time), the function returns directly. If the priority is less than the current callback, cancel its callback (canceldeferredcallback (callback ID))
2. Calculate the timeout and then scheduleDeferredCallback(performAsyncWork, {timeout})
function scheduleCallbackWithExpirationTime( root: FiberRoot, expirationTime: ExpirationTime, ) { // Determine whether the last callback is completed if (callbackExpirationTime !== NoWork) { // A callback is already scheduled. Check its expiration time (timeout). // Current task exits if priority is less than previous task if (expirationTime > callbackExpirationTime) { // Existing callback has sufficient timeout. Exit. return; } else { // Otherwise, cancel the last callback if (callbackID !== null) { // Existing callback has insufficient timeout. Cancel and schedule a // new one. cancelDeferredCallback(callbackID); } } // The request callback timer is already running. Don't start a new one. } else { // There is no previous callback to be executed. Start timer. This function is used for devtool. startRequestCallbackTimer(); } callbackExpirationTime = expirationTime; // The current performance.now() is subtracted from the performance.now() when the program was just executed const currentMs = now() - originalStartTimeMs; // Converted to ms const expirationTimeMs = expirationTimeToMs(expirationTime); // The delayed expiration time of the current task, which is derived from the expiration time - the creation time of the current task. When it exceeds the expiration time, it means that the task needs to be forced to update. const timeout = expirationTimeMs - currentMs; // Generate a callback ID to close the task callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); }
scheduleDeferredCallback
The scheduleDeferredCallback function is unstable in yes: Scheduler.js.
1. Create a task node, newNode, and insert the callback list according to the priority.
2. We have arranged the tasks according to the expiration time, so when should we execute the tasks? How to implement it? The answer is that there are two situations: 1. When the first task node is added, the task execution starts; 2. When the newly added task replaces the previous node and becomes the new first node. Because 1 means the task starts from scratch and should be started immediately. 2 means that a new task with the highest priority is coming. You should stop the previous task and start the new task again. The above two cases correspond to the execution of the ensureHostCallbackIsScheduled method.
function unstable_scheduleCallback(callback, deprecated_options) { var startTime = currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime(); // In fact, only the first if condition will be entered here, because if the external write is dead, it will definitely be passed to deprecated [options. Timeout]. // The smaller the priority is, the higher the priority is, and it also represents the expiration time of a task. var expirationTime; if ( typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number' ) { // FIXME: Remove this branch once we lift expiration times out of React. expirationTime = startTime + deprecated_options.timeout; } else { switch (currentPriorityLevel) { case ImmediatePriority: expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT; break; case UserBlockingPriority: expirationTime = startTime + USER_BLOCKING_PRIORITY; break; case IdlePriority: expirationTime = startTime + IDLE_PRIORITY; break; case NormalPriority: default: expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT; } } // Circular double linked list structure var newNode = { callback, priorityLevel: currentPriorityLevel, expirationTime, next: null, previous: null, }; // Insert the new callback into the list, ordered first by expiration, then // by insertion. So the new callback is inserted any other callback with // equal expiration. // The core idea is that firstCallbackNode has the highest priority and lastCallbackNode has the lowest priority. // After a new node is generated, compare priorities from the beginning // If the new one is high, insert the new one forward. Otherwise, insert the new one backward until no node has a lower priority. // Then the new node becomes lastCallbackNode // In the case of changing the firstCallbackNode, it needs to be rescheduled if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = newNode.next = newNode.previous = newNode; ensureHostCallbackIsScheduled(); } else { var next = null; var node = firstCallbackNode; do { if (node.expirationTime > expirationTime) { // The new callback expires before this one. next = node; break; } node = node.next; } while (node !== firstCallbackNode); if (next === null) { // No callback with a later expiration was found, which means the new // callback has the latest expiration in the list. next = firstCallbackNode; } else if (next === firstCallbackNode) { // The new callback has the earliest expiration in the entire list. firstCallbackNode = newNode; ensureHostCallbackIsScheduled(); } var previous = next.previous; previous.next = next.previous = newNode; newNode.next = next; newNode.previous = previous; } return newNode; }
ensureHostCallbackIsScheduled
1. Judge whether there is a host callback. If cancelHostCallback() has been saved, then request hostcallback (flushWork, expirationtime) will be started. The incoming flushWork is the function of the flushing task (later explained) and the expiration time of the task node of the team leader. Here, instead of executing flushWork immediately, we give it to requestHostCallback. Because we don't want to execute all the tasks in the task list at once, or all the tasks in the list at once. JS is a single thread. We always occupy the main thread when executing these tasks, which will cause other tasks of the browser to wait all the time. For example, animation will get stuck, so we need to choose the right time to execute it. So we left it to requestHostCallback to deal with it and flushWork to it. Here you can temporarily think of flushWork as performing tasks in the linked list.
Note: let's think about it here. We need to ensure the smoothness of the application, because the browser renders one frame at a time. After each frame is rendered, there will be some free time to perform other tasks, so we want to use this free time to perform our tasks. So we immediately think of a native api: request idlecallback. But for some reason, the react team abandoned the api and used the requestAnimationFrame and MessageChannel pollyfill to create a requestIdleCallback.
function ensureHostCallbackIsScheduled() { // Scheduling is in progress, so you can't interrupt what is already in progress. if (isExecutingCallback) { // Don't schedule work yet; wait until the next time we yield. return; } // Schedule the host callback using the earliest expiration in the list. // Make the highest priority to schedule. If there is a scheduled cancellation var expirationTime = firstCallbackNode.expirationTime; if (!isHostCallbackScheduled) { isHostCallbackScheduled = true; } else { // Cancel the existing host callback. // Cancel the callback being scheduled cancelHostCallback(); } // Initiating scheduling requestHostCallback(flushWork, expirationTime); }
requestHostCallback
1. Here are two global variables: scheduledHostCallback and timeoutTime.
Represents the callback and expiration time of the first task, respectively.
2. When entering this function, you will immediately judge whether the current task is overdue. If it is overdue, don't say anything. Go and execute it immediately, regardless of whether the browser is empty or not. If the browser is not empty, you have to execute it for me. This task is proposed by Party A, and the delivery deadline has passed. Then we will do it quickly. Party A's father is God. Here's a question: is it to directly execute the flushWork we passed in?
3. If the task hasn't expired and the delivery time hasn't arrived, it's OK. Take your time. When the browser is free, we are doing it. After all, we are very busy, so we can delay it. So for non urgent tasks, we will give the request animation framework with timeout (animation tick).
requestHostCallback = function(callback, absoluteTimeout) { scheduledHostCallback = callback; timeoutTime = absoluteTimeout; // Isflushing hostcallback is only set to true in channel.port1.onmessage // Isflushing hostcallback indicates that the added task needs to be executed immediately // That is to say, when a task is in progress or a new task has passed its expiration date // Perform a new task now, instead of waiting for the next frame if (isFlushingHostCallback || absoluteTimeout < 0) { // Don't wait for the next frame. Continue working ASAP, in a new event. // Send a message, channel.port1.onmessage will listen to the message and execute window.postMessage(messageKey, '*'); } else if (!isAnimationFrameScheduled) { // If rAF didn't already schedule one, we need to schedule a frame. // TODO: If this rAF doesn't materialize because the browser throttles, we // might want to still have setTimeout trigger rIC as a backup to ensure // that we keep performing work. // If isAnimationFrameScheduled is set to true, you will not enter this branch again. // But there will be internal mechanism to ensure the implementation of callback. isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } };
requestAnimationFrameWithTimeout
This function can be understood as the optimized requestAnimationFrame.
1. When we call requestAnimationFrameWithTimeout and pass in a callback, we will start a requestAnimationFrame and a setTimeout, both of which will execute the callback. However, due to the relatively high execution priority of the requestAnimationFrame, it internally calls clearTimeout to cancel the following timer operations. So in the case of active page, the performance is consistent with the request animation frame.
2. The requestAnimationFrame does not work when the page is switched to inactive. At this time, the requestAnimationFrameWithTimeout is equivalent to starting a 100ms timer to take over the task execution. The execution frequency is not high or low, which not only does not affect the cpu energy consumption, but also ensures that the task can be executed efficiently.
Wait a moment. When we used to use the requestAnimationFrame, we need to call ourselves circularly, otherwise we only execute it once... ... where is it recursively called? We are carefully observing that this function passes in a parameter callback, which is the animationTick passed in by the previous function. What is this? Haven't you seen it?
var ANIMATION_FRAME_TIMEOUT = 100; var rAFID; var rAFTimeoutID; var requestAnimationFrameWithTimeout = function(callback) { // schedule rAF and also a setTimeout // The functions starting with local here refer to request Animation Frame and setTimeout. // request Animation Frame callbacks only when the page is in the foreground // If the page will not execute the callback in the background, the callback will be guaranteed through setTimeout. // Both callbacks can cancel each other // callback refers to animationTick rAFID = localRequestAnimationFrame(function(timestamp) { // cancel the setTimeout localClearTimeout(rAFTimeoutID); callback(timestamp); }); rAFTimeoutID = localSetTimeout(function() { // cancel the requestAnimationFrame localCancelAnimationFrame(rAFID); callback(getCurrentTime()); }, ANIMATION_FRAME_TIMEOUT); };
animationTick
1. If there is a task, recursively request the next frame. If there is no task, it can be ended. Exit recursion.
2. Here are several important global variables:
The initial value of frameDeadline is 0, and the deadline of the current frame is calculated.
The initial value of activeFrameTime is 33, and the rendering time of a frame is 33ms. Here, assume 1s 30 frames.
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
rafTime is the parameter passed in to this function, which is the time stamp at the beginning of the current frame. nextFrameTime represents the rendering time of the actual frame (except for the first execution). The activeFrameTime is then updated based on this value
. Dynamically adjust the rendering time of each frame according to different environments to achieve the refresh rate of the system.
3. At the end of the callback function of each frame, window.postMessage(messageKey, ''); What? What is this? Shouldn't flushWork be called to perform the task? There is also a question mentioned above. In the request host callback, if the task expires, execute the task immediately. Does he perform flushWork? Let's take a look: in the previous requestHostCallback function, take a big look: window.postMessage(messageKey, ""); what??? This method is also implemented.
var animationTick = function(rafTime) { if (scheduledHostCallback !== null) { // Eagerly schedule the next animation callback at the beginning of the // frame. If the scheduler queue is not empty at the end of the frame, it // will continue flushing inside that callback. If the queue *is* empty, // then it will exit immediately. Posting the callback at the start of the // frame ensures it's fired within the earliest possible frame. If we // waited until the end of the frame to post the callback, we risk the // browser skipping a frame and not firing the callback until the frame // after that. // Continue recursion if scheduledHostCallback is not empty // But note that the recursion here is not synchronous, and the animation tick will be executed in the next frame. requestAnimationFrameWithTimeout(animationTick); } else { // No pending work. Exit. isAnimationFrameScheduled = false; return; } // rafTime is performance.now(), no matter which timer is executed. // If we apply animationTick for the first time, then frameDeadline = 0 activeFrameTime = 33 // That is to say, nextFrameTime = performance.now() + 33 // For later calculation, we assume nextFrameTime = 5000 + 33 = 5033 // Then why is activeFrameTime 33? Because React assumes that your refresh rate is 30 Hz. // One second corresponds to 1000 milliseconds, 1000 / 30 ≈ 33 // -------------------------------The following notes are for the second time // The second time I come in here to execute, because the animationTick callback must be executed in the next frame, if our screen is a refresh rate of 60 Hz // So the time of a frame is 1000 / 60 ≈ 16 // At this time, nextFrameTime = 5000 + 16 - 5033 + 33 = 16 // -------------------------------The following notes are for the third time // nextFrameTime = 5000 + 16 * 2 - 5048 + 33 = 17 var nextFrameTime = rafTime - frameDeadline + activeFrameTime; // This if condition must not enter for the first time // -------------------------------The following notes are for the second time // At this time, 16 < 33 & & 5033 < 33 = false, that is to say, the if condition still cannot be entered at the second frame. // -------------------------------The following notes are for the third time // At this time, 17 < 33 & & 16 < 33 = true, which means that if the refresh rate is greater than 30 Hz, the active frame time will not be adjusted until two frames. if ( nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime ) { // The reason why it is less than 8 is that it can't handle browsers with refresh rate higher than 120 hz. if (nextFrameTime < 8) { // Defensive coding. We don't support higher frame rates than 120hz. // If the calculated frame time gets lower than 8, it is probably a bug. nextFrameTime = 8; } // If one frame goes long, then the next one can be short to catch up. // If two frames are short in a row, then that's an indication that we // actually have a higher frame rate than what we're currently optimizing. // We adjust our heuristic dynamically accordingly. For example, if we're // running on 120hz display or 90hz VR display. // Take the max of the two in case one of them was an anomaly due to // missed frame deadlines. // After the third frame comes in, activeframetime = 16 < 17? 16: 17 = 16 // And then next time it's 16 milliseconds per frame. activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; } else { // First time in 5033 // Coming in for the second time 16 previousFrameTime = nextFrameTime; } // First frameDeadline = 5000 + 33 = 5033 // -------------------------------The following notes are for the second time // frameDeadline = 5016 + 33 = 5048 frameDeadline = rafTime + activeFrameTime; // Make sure there is no more postMessage in this frame // postMessage belongs to macro task // const channel = new MessageChannel(); // const port = channel.port2; // channel.port1.onmessage = function(event) { // console.log(1) // } // requestAnimationFrame(function (timestamp) { // setTimeout(function () { // console.log('setTimeout') // }, 0) // port.postMessage(undefined) // Promise.resolve(1).then(function (value) { // console.log(value, 'Promise') // }) // }) // The output order of the above code is promise - > onmessage - > setTimeout // It can be seen from this that micro tasks are executed first, followed by macro tasks, and there are also order in macro tasks. // onmessage takes precedence over setTimeout callbacks // For browsers, when we execute the request Animation Frame callback, // It will first render the page, then determine whether to execute the micro task, and finally execute the macro task, and then execute onmessage first. // Of course, the faster macro task than onmessage is set Immediate, but this API can only be used under IE. if (!isMessageEventScheduled) { isMessageEventScheduled = true; window.postMessage(messageKey, '*'); } };
window.postMessage(messageKey, '*')
1. In fact, we want to have a problem. What we want is to perform the rendering task of the browser first in each frame. If we have free time after performing the rendering task of this frame, we are performing our task.
2. If you start the task directly here, it will be executed at the beginning of this frame. Do you want to occupy one frame to execute your task? Isn't that what I said above in vain...
3. So we use window.postMessage, which is macrotask. The call time of onmessage's callback function is after the completion of a frame's paint, which is exactly used by the react scheduler to execute the task in the remaining time after the completion of a frame's rendering.
4. The monitoring of window.addEventListener('message', idleTick, false) corresponding to window.postMessage(messageKey,' * ') will trigger the call of idleTick function.
4. So let's take a look at idleTick. Our task must be executed in this event callback.
var messageKey = '__reactIdleCallback$' + Math.random() .toString(36) .slice(2); var idleTick = function(event) { if (event.source !== window || event.data !== messageKey) { return; } // Setting of some variables isMessageEventScheduled = false; var prevScheduledCallback = scheduledHostCallback; var prevTimeoutTime = timeoutTime; scheduledHostCallback = null; timeoutTime = -1; // Get current time var currentTime = getCurrentTime(); var didTimeout = false; // Determine whether the previously calculated time is less than the current time. If the time exceeds the current time, it means that the execution time of tasks such as browser rendering has exceeded one frame, and there is no idle time for this frame. if (frameDeadline - currentTime <= 0) { // There's no time left in this idle period. Check if the callback has // a timeout and whether it's been exceeded. // Judge whether the current task is overdue if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) { // Exceeded the timeout. Invoke the callback even though there's no // time left. didTimeout = true; } else { // No timeout. // If it doesn't expire, it will be dropped to the next frame for execution. if (!isAnimationFrameScheduled) { // Schedule another animation callback so we retry later. isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } // Exit without invoking the callback. scheduledHostCallback = prevScheduledCallback; timeoutTime = prevTimeoutTime; return; } } // Finally, execute flushWork. The callback s involved here are all flushWork. if (prevScheduledCallback !== null) { isFlushingHostCallback = true; try { prevScheduledCallback(didTimeout); } finally { isFlushingHostCallback = false; } } };
flushWork
Let's think about it. Will this flushWork be a simple implementation of the task chain from the beginning to the end? If that's the case, isn't a lot of bb above me talking for nothing... It's all done in one go. What about performance optimization? Back to before liberation. So, it's not as simple as we think.
1. flushWork has two processing logics according to the didTimeout parameter. If it is true, all overdue tasks in the task list will be executed once; if it is false, as many tasks as possible will be executed before the current frame expires.
2. Finally, if there are still tasks, start a new round of task execution scheduling, ensureHostCallbackIsScheduled(), to reset the callback list. Reset all the scheduling constants, and the old callback will not be executed.
3. The execution task here is to call flushFirstCallback to execute the task with the highest priority in the callback.
function flushWork(didTimeout) { // Setting of some variables isExecutingCallback = true; deadlineObject.didTimeout = didTimeout; try { // Judge whether it is timeout if (didTimeout) { // Flush all the expired callbacks without yielding. while (firstCallbackNode !== null) { // Read the current time. Flush all the callbacks that expire at or // earlier than that time. Then read the current time again and repeat. // This optimizes for as few performance.now calls as possible. // In case of timeout, obtain the current time and judge whether the task expires. In case of expiration, execute the task. // And judge whether the next task has expired. var currentTime = getCurrentTime(); if (firstCallbackNode.expirationTime <= currentTime) { do { flushFirstCallback(); } while ( firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime ); continue; } break; } } else { // Keep flushing callbacks until we run out of time in the frame. // If there is no timeout, there is still time to execute the task. Continue to judge after the task is completed. if (firstCallbackNode !== null) { do { flushFirstCallback(); } while ( firstCallbackNode !== null && getFrameDeadline() - getCurrentTime() > 0 ); } } } finally { isExecutingCallback = false; if (firstCallbackNode !== null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(); } else { isHostCallbackScheduled = false; } // Before exiting, flush all the immediate work that was scheduled. flushImmediateWork(); } }
flushFirstCallback
This is the link list operation. After executing the first callback, delete the callback from the link list.
The current task node flushedNode.callback is called here. What is our callback? Time began to flow backward. Back to the scheduleCallbackWithExpirationTime function scheduleDeferredCallback(performAsyncWork, {timeout}) I believe you still have an impression on this. It is actually the entry function for us to enter Scheduler.js. If it is passed back to performAsyncWork as a callback function, that is the callback function that is called in this function.
function flushFirstCallback() { var flushedNode = firstCallbackNode; // Remove the node from the list before calling the callback. That way the // list is in a consistent state even if the callback throws. // Linked list operation var next = firstCallbackNode.next; if (firstCallbackNode === next) { // This is the last callback in the list. // There is only one node in the current linked list firstCallbackNode = null; next = null; } else { // There are multiple nodes. Reassign the firstCallbackNode for the next while judgment in the previous function. var lastCallbackNode = firstCallbackNode.previous; firstCallbackNode = lastCallbackNode.next = next; next.previous = lastCallbackNode; } // Empty pointer flushedNode.next = flushedNode.previous = null; // Now it's safe to call the callback. // This callback is the performAsyncWork function var callback = flushedNode.callback; var expirationTime = flushedNode.expirationTime; var priorityLevel = flushedNode.priorityLevel; var previousPriorityLevel = currentPriorityLevel; var previousExpirationTime = currentExpirationTime; currentPriorityLevel = priorityLevel; currentExpirationTime = expirationTime; var continuationCallback; try { // Execute callback function continuationCallback = callback(deadlineObject); } finally { currentPriorityLevel = previousPriorityLevel; currentExpirationTime = previousExpirationTime; } // ...... }
Here is a place to note that when calling the callback of a task, we passed in an object: deadlineObject.
Time remaining: how much free time does the current frame have?
didTimeout: whether the task expires
var deadlineObject = { timeRemaining, didTimeout: false, };
This deadlineObject is a global object, mainly used for the shouldYield function.
The deadlock in the function is this object.
function shouldYield() { if (deadlineDidExpire) { return true; } if ( deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork ) { // Disregard deadline.didTimeout. Only expired work should be flushed // during a timeout. This path is only hit for non-expired work. return false; } deadlineDidExpire = true; return true; }
performAsyncWork
1, this function gets a parameter dl, which is the deadlineObject that was invoked before calling the callback function.
2. Call performWork(NoWork, dl); the first parameter is minExpirationTime, where NoWork=0 is passed in, and the second parameter Deadline=dl.
function performAsyncWork(dl) { // Judge whether the task is overdue if (dl.didTimeout) { // The callback timed out. That means at least one update has expired. // Iterate through the root schedule. If they contain expired work, set // the next render expiration time to the current time. This has the effect // of flushing all expired work in a single batch, instead of flushing each // level one at a time. if (firstScheduledRoot !== null) { recomputeCurrentRendererTime(); let root: FiberRoot = firstScheduledRoot; do { didExpireAtExpirationTime(root, currentRendererTime); // The root schedule is circular, so this is never null. root = (root.nextScheduledRoot: any); } while (root !== firstScheduledRoot); } } performWork(NoWork, dl); }
I need to insert a sentence here. Remember if it is synchronization in requestWork? Back to this function, let's see if it is synchronous, call performSyncWork directly. performSyncWork and performSyncWork are so similar. Are they separated brothers for many years? Go to performSyncWork and have a look, um... Yes, he and performAsyncWork called the same method, but the parameters passed were different. performWork(Sync, null); the first parameter he passed in was Sync=1. The second parameter is null.
In the requestWork function:
if (expirationTime === Sync) { // synchronization performSyncWork(); } else { // Asynchronous, start scheduling scheduleCallbackWithExpirationTime(root, expirationTime); }
function performSyncWork() { performWork(Sync, null); }
performWork (perform task)
1. If it is synchronization (deadline == null), whether there is free time for frame rendering is not considered at all, and synchronization task has no expiration time. Traverse all roots, and execute all synchronous tasks in root.
Note: there may be multiple root s, that is, ReactDOM.render may be called multiple times.
2. If it is asynchronous (deadline! = = null), traverse all roots and execute all overdue tasks in root, because overdue tasks must be executed. If the frame has free time, perform as many tasks as possible.
3. In the above two cases, the task has been executed. What method have they called? performWorkOnRoot.
// currentRendererTime calculates the number of milliseconds since the page was loaded // The currentSchedulerTime is also the time when it is loaded to the current time. isRendering === true is used as the fixed value to return, otherwise, every time the requestCurrentTime will recalculate the new time. function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) { // Note here that the deadlock points to the incoming deadlock object object (dl) deadline = dl; // Keep working on roots until there's no more work, or until we reach // the deadline. // Find the next root with the highest priority to render: nextflushedroot and the corresponding expiritaiontime: nextflushedexpirationtime findHighestPriorityRoot(); // asynchronous if (deadline !== null) { // Recalculate currentrenderetime recomputeCurrentRendererTime(); currentSchedulerTime = currentRendererTime; // ...... while ( nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime) && // deadlineDidExpire to determine whether the time slice is overdue, and to judge in shouldYield // Current renderertime compare nextFlushedExpirationTime to determine whether the task has timed out // Currentrenderertime > = nextflushedexpirationtime timed out (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime) ) { performWorkOnRoot( nextFlushedRoot, nextFlushedExpirationTime, currentRendererTime >= nextFlushedExpirationTime, ); findHighestPriorityRoot(); recomputeCurrentRendererTime(); currentSchedulerTime = currentRendererTime; } } else { // synchronization while ( nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && // In general, minExpirationTime should be the same as nextFlushedExpirationTime because they all come from the same root. nextFlushedExpirationTime is the root.xpirationtime read out in the findhighestpriorityreoot stage. (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime) ) { performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true); findHighestPriorityRoot(); } } // We're done flushing work. Either we ran out of time in this callback, // or there's no more work left with sufficient priority. // If we're inside a callback, set this to false since we just completed it. if (deadline !== null) { callbackExpirationTime = NoWork; callbackID = null; } // If there's work left over, schedule a new callback. if (nextFlushedExpirationTime !== NoWork) { scheduleCallbackWithExpirationTime( ((nextFlushedRoot: any): FiberRoot), nextFlushedExpirationTime, ); } // Clean-up. deadline = null; deadlineDidExpire = false; finishRendering(); }
performWorkOnRoot
1. First, describe the two stages of task execution:
renderRoot render phase
Completeloot commit phase
2. If it is synchronization or the task has expired, render root first (pass in parameter isYieldy=false, which means the task cannot be interrupted), and then complete root.
3. If it is asynchronous, first renderRoot (the input parameter isYieldy=true, which means the task can be interrupted). After that, see if there is any free time in this frame. If there is no time, complete root can only wait for the next frame.
4. Before calling renderRoot in steps 2 and 3, one more thing will be done to judge the finishedWork! = = null, because the previous time slice may have finished renderRoot and no time to complete root. If there is finished finished finished work of renderRoot in this time slice, it will directly complete root.
function performWorkOnRoot( root: FiberRoot, expirationTime: ExpirationTime, isExpired: boolean, ) { // ...... isRendering = true; // Check if this is async work or sync/expired work. if (deadline === null || isExpired) { // Synchronization or task has expired. Do not interrupt the task // Flush work without yielding. // TODO: Non-yieldy work does not necessarily imply expired work. A renderer // may want to perform some work without yielding, but also without // requiring the root to complete (by triggering placeholders). // Determine whether there is a finished finished work, and complete it if there is one let finishedWork = root.finishedWork; if (finishedWork !== null) { // This root is already complete. We can commit it. completeRoot(root, finishedWork, expirationTime); } else { root.finishedWork = null; // If this root previously suspended, clear its existing timeout, since // we're about to try rendering again. const timeoutHandle = root.timeoutHandle; if (timeoutHandle !== noTimeout) { root.timeoutHandle = noTimeout; // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above cancelTimeout(timeoutHandle); } const isYieldy = false; // Otherwise, render it to DOM. renderRoot(root, isYieldy, isExpired); finishedWork = root.finishedWork; if (finishedWork !== null) { // We've completed the root. Commit it. completeRoot(root, finishedWork, expirationTime); } } } else { // Asynchronous task has not expired, can be interrupted // Flush async work. let finishedWork = root.finishedWork; if (finishedWork !== null) { // This root is already complete. We can commit it. completeRoot(root, finishedWork, expirationTime); } else { root.finishedWork = null; // If this root previously suspended, clear its existing timeout, since // we're about to try rendering again. const timeoutHandle = root.timeoutHandle; if (timeoutHandle !== noTimeout) { root.timeoutHandle = noTimeout; // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above cancelTimeout(timeoutHandle); } const isYieldy = true; renderRoot(root, isYieldy, isExpired); finishedWork = root.finishedWork; if (finishedWork !== null) { // We've completed the root. Check the deadline one more time // before committing. if (!shouldYield()) { // Still time left. Commit the root. completeRoot(root, finishedWork, expirationTime); } else { // There's no time left. Mark this root as complete. We'll come // back and commit it later. root.finishedWork = finishedWork; } } } } isRendering = false; }
renderRoot & completeRoot
After that, we enter these two phases of component update, which will be explained in detail in the following chapters.
No matter how complicated the world is, it still remains lovely.
I'm the grapefruit fairy. If there is something wrong with the article, please correct it.~