Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”. This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
React17 React17 React17 React17 React17 React17
In this chapter we’ll look at the Render phase, one of the core phases of React, and explore the source code for some of the following:
- Update task trigger
- Update task creation
- The Reconciler process traverses and performs tasks synchronously and asynchronously
- How does scheduler implement frame idle time scheduling and interrupt tasks
Triggered update
ReactDOM. Render, setState, forUpdate, and useState in hooks. We’ll talk about hooks later, but we’ll focus on the first three.
ReactDOM.render
Reactdom. render, as the entry function of the React application, is triggered when a page is rendered for the first time. The first dom creation of the page is also one of the situations that trigger the React update. The overall process is as follows:
First call legacyRenderSubtreeIntoContainer function, check the root node root exists, if there is no, Call legacyCreateRootFromDOMContainer create a root node root, rootFiber and fiberRoot and bind the reference relationship between them, and then call updateContainer without batch update process behind the execution; If yes, call updateContainer directly to batch execute the following update process:
// packages/react-dom/src/client/ReactDOMLegacy.js
function legacyRenderSubtreeIntoContainer(parentComponent: ? React$Component<any.any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function.) {
// ...
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if(! root) {// The root node does not exist when first rendered
/ / create a root node, through legacyCreateRootFromDOMContainer fiberRoot and rootFiber
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Execute the update process without batch
unbatchedUpdates(() = > {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Execute the update process in batches
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
Copy the code
The updateContainer function does the following:
- RequestEventTime: Gets the time when the update is triggered
- RequestUpdateLane: obtains the current task priority
- CreateUpdate: Creates an update
- EnqueueUpdate: Pushes the task to the update queue
- ScheduleUpdateOnFiber: Scheduling updates
We’ll talk more about these functions later
// packages/react-dom/src/client/ReactDOMLegacy.js
export function updateContainer(element: ReactNodeList, container: OpaqueRoot, parentComponent: ? React$Component<any.any>,
callback: ?Function.) :Lane {
// ...
const current = container.current;
const eventTime = requestEventTime(); // Get the update trigger time
// ...
const lane = requestUpdateLane(current); // Get the task priority
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// ...
const update = createUpdate(eventTime, lane); // Create an update task
update.payload = {element};
callback = callback === undefined ? null : callback;
if(callback ! = =null) {
// ...
update.callback = callback;
}
enqueueUpdate(current, update); // Push the task to the update queue
scheduleUpdateOnFiber(current, lane, eventTime); // set the schedule to work
return lane;
}
Copy the code
setState
SetState is the most commonly used method to modify the state in class components. The state modification will trigger the update process, and the execution process is as follows:
The class component defines the setState method on the prototype chain, which calls the enqueueSetState method on the trigger updater:
// packages/react/src/ReactBaseClasses.js
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null.'setState(...) : takes an object of state variables to update or a ' +
'function which returns an object of state variables.',);this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Copy the code
Then we look at the enqueueSetState method defined on the following updater. As soon as we see this, we know that it does almost exactly what the updateContainer method does, which is to trigger subsequent update scheduling.
// packages/react-reconciler/src/ReactFiberClassComponent.old.js
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime(); // Get the update trigger time
const lane = requestUpdateLane(fiber); // Get the task priority
const update = createUpdate(eventTime, lane); // Create an update task
update.payload = payload;
if(callback ! = =undefined&& callback ! = =null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
enqueueUpdate(fiber, update); // Push the task to the update queue
scheduleUpdateOnFiber(fiber, lane, eventTime); // set the schedule to work
// ...
if(enableSchedulingProfiler) { markStateUpdateScheduled(fiber, lane); }},// ...
};
Copy the code
forceUpdate
ForceUpdate flows almost exactly like setState:
It also calls the enqueueForceUpdate method on the trigger updater, which also triggers a series of updates:
reconciler/src/ReactFiberClassComponent.old.js
const classComponentUpdater = {
isMounted,
// ...
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime(); // Get the update trigger time
const lane = requestUpdateLane(fiber); // Get the task priority
const update = createUpdate(eventTime, lane); // Create an update
update.tag = ForceUpdate;
if(callback ! = =undefined&& callback ! = =null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
update.callback = callback;
}
enqueueUpdate(fiber, update); // Push the task forward to update the queue
scheduleUpdateOnFiber(fiber, lane, eventTime); // Triggers update scheduling
// ...
if(enableSchedulingProfiler) { markForceUpdateScheduled(fiber, lane); }}};Copy the code
Creating an Update Task
It can be found that the above three actions to trigger the update eventually lead to the same path, and all go through the process from requestEventTime to scheduleUpdateOnFiber in the above flowchart to create the update task. Let’s take a look at how the update task is created in detail.
Gets the update trigger time
As mentioned in the previous article, react performs update tasks separately and executes high-priority tasks in each frame to ensure a smooth user experience. So even for tasks with the same priority, which one should be executed first in the case of multiple tasks?
React creates a currentEventTime using the requestEventTime method, which identifies the time when the update task is triggered. Tasks with the same time are executed in batches. For tasks of the same priority, the smaller the currentEventTime value is, the earlier the task is executed.
Let’s look at the implementation of the requestEventTime method:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export function requestEventTime() {
if((executionContext & (RenderContext | CommitContext)) ! == NoContext) {// During react execution, the current time is returned directly
return now();
}
// If react is not being executed
if(currentEventTime ! == NoTimestamp) {// Executing browser event to return last currentEventTime
return currentEventTime;
}
// React first update after interrupt, computes new currentEventTime
currentEventTime = now();
return currentEventTime;
}
Copy the code
In this method, (– an optional executionContext & (RenderContext | CommitContext) do the binary operations, RenderContext represents the react is calculated to update, CommitContext represents that react is committing updates. React (now()); react (now());
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export const NoContext = / * * / 0b0000000;
const BatchedContext = / * * / 0b0000001;
const EventContext = / * * / 0b0000010;
const DiscreteEventContext = / * * / 0b0000100;
const LegacyUnbatchedContext = / * * / 0b0001000;
const RenderContext = / * * / 0b0010000;
const CommitContext = / * * / 0b0100000;
export const RetryAfterError = / * * / 0b1000000;
let executionContext: ExecutionContext = NoContext;
Copy the code
When the time difference between the current and later update tasks is less than 10ms, the last Scheduler_now is directly used, which can erase the time difference between update tasks within 10ms and facilitate batch update:
// packages/react-reconciler/src/SchedulerWithReactIntegration.old.js
export const now =
initialTimeMs < 10000 ? Scheduler_now : () = > Scheduler_now() - initialTimeMs;
Copy the code
To sum up, requestEvent does the following:
- In render and Commit phases of React, we directly obtain the trigger time of update tasks and erase the update tasks within 10ms to facilitate batch execution.
- CurrentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime: currentEventTime
- If it is the first update since react last interrupted, give currentEventTime a new value
Prioritize update tasks
Having said the trigger time for tasks of the same priority, how are tasks prioritized? RequestUpdateLane = requestUpdateLane = requestUpdateLane
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export function requestUpdateLane(fiber: Fiber) :Lane {
// ...
// Get the priority of task scheduling according to the priority of recorded events
const schedulerPriority = getCurrentPriorityLevel();
// ...
let lane;
if( (executionContext & DiscreteEventContext) ! == NoContext && schedulerPriority === UserBlockingSchedulerPriority ) {/ / if the user block level of events, are calculated by InputDiscreteLanePriority update priority lane
lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
} else {
// Otherwise, the schedulerLanePriority is calculated based on the event priority
const schedulerLanePriority = schedulerPriorityToLanePriority(
schedulerPriority,
);
if (decoupleUpdatePriorityFromScheduler) {
const currentUpdateLanePriority = getCurrentUpdateLanePriority();
// Based on the calculated schedulerLanePriority, the update priority lane is calculated
lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
}
return lane;
}
Copy the code
It first finds the priority schedulerPriority of the task Scheduler that will be retrieved based on the event priority recorded in the Scheduler using the getCurrentPriorityLevel method. Then, lane is calculated by findUpdateLane method as the priority in the update process.
In findUpdateLane, different levels of lane are matched according to the event type. The priority of event type is classified as follows. The higher the value is, the higher the priority is:
// packages/react-reconciler/src/ReactFiberLane.js
export const SyncLanePriority: LanePriority = 15;
export const SyncBatchedLanePriority: LanePriority = 14;
const InputDiscreteHydrationLanePriority: LanePriority = 13;
export const InputDiscreteLanePriority: LanePriority = 12;
const InputContinuousHydrationLanePriority: LanePriority = 11;
export const InputContinuousLanePriority: LanePriority = 10;
const DefaultHydrationLanePriority: LanePriority = 9;
export const DefaultLanePriority: LanePriority = 8;
const TransitionHydrationPriority: LanePriority = 7;
export const TransitionPriority: LanePriority = 6;
const RetryLanePriority: LanePriority = 5;
const SelectiveHydrationLanePriority: LanePriority = 4;
const IdleHydrationLanePriority: LanePriority = 3;
const IdleLanePriority: LanePriority = 2;
const OffscreenLanePriority: LanePriority = 1;
export const NoLanePriority: LanePriority = 0;
Copy the code
Creating an update object
CreateUpdate creates an update object based on eventTime and Lane created by the above two methods:
// packages/react-reconciler/src/ReactUpdateQueue.old.js
export function createUpdate(eventTime: number, lane: Lane) :Update< * >{
const update: Update<*> = {
eventTime, // Update the event to go
lane, / / priority
tag: UpdateState, // Specify the update type, 0 update, 1 replace, 2 force update, 3 capture update
payload: null.// What to update, such as the first argument received by setState
callback: null.// Callback after update
next: null.// Points to the next update
};
return update;
}
Copy the code
Associate the update queue for fiber
Once the update object is created, call enqueueUpdate to place the update object in the associated Fiber’s updateQueue queue:
// packages/react-reconciler/src/ReactUpdateQueue.old.js
export function enqueueUpdate<State> (fiber: Fiber, update: Update<State>) {
// Get the update queue for the current fiber
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// If updateQueue is empty, fiber is not rendered yet
return;
}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
const pending = sharedQueue.pending;
if (pending === null) {
// Pending is null, indicating the first update
update.next = update;
} else {
// Insert update into the loop list
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
// ...
}
Copy the code
The reconciler process
With the update tasks created and linked to Fiber above, it is time for the React Render phase, which is at the heart of the Reconciler phase.
Perform different updates based on the task type
The Reconciler phase coordinates the execution of the reconciler tasks. ScheduleUpdateOnFiber is the entry function, and the checkForNestedUpdates method is first called to check the number of nested updates. If the number of nested updates is greater than 50, It is considered a cyclic update. An exception is thrown, avoiding an infinite loop such as a setState call in the render function of the class component.
Then advance by markUpdateLaneFromFiberToRoot method, recursive update fiber lane, lane of the update is very simple, is the current task lane to lane of binary or computing stack.
Let’s take a look at the source code:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number.) {
// Check if there is a loop update
// Avoid cases where setState is called in an infinite loop in the render function of the class component
checkForNestedUpdates();
// ...
// Update child-.fiberlanes from bottom up
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
// ...
// Mark root to have an update and insert root.pendingLanes into the update lane
markRootUpdated(root, lane, eventTime);
if (lane === SyncLane) { // Synchronize tasks, using synchronous rendering
if( (executionContext & LegacyUnbatchedContext) ! == NoContext && (executionContext & (RenderContext | CommitContext)) === NoContext ) {// If this is a synchronous update and the rendering has not started yet
// Indicates that the main js thread is idle and no react task is running
// ...
// Call performSyncWorkOnRoot to perform a synchronous update task
performSyncWorkOnRoot(root);
} else {
// If the react task is being executed while the update is being synchronized
// Call ensureRootIsScheduled to reuse the currently executing task to ensure that the update is executed together
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
// ...
} else {
// If this update is an asynchronous task
// ...
// call ensureRootIsScheduled to perform interruptible updates
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
mostRecentlyUpdatedRoot = root;
}
Copy the code
The react execution phase of the current thread determines the type of update to be performed:
Performing synchronous updates
If the task type is a synchronization task and the main JS thread is idle (no react task is being executed), the performSyncWorkOnRoot(root) method is used to start the synchronization task.
PerformSyncWorkOnRoot does two main things:
- RenderRootSync synchronizes rendering tasks from the root node
- CommitRoot performs the commit process
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function performSyncWorkOnRoot(root) {
// ...
exitStatus = renderRootSync(root, lanes);
// ...
commitRoot(root);
// ...
}
Copy the code
When the task type is synchronous but the JS main thread is not idle. The ensureRootIsScheduled method is implemented:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...
// If there are tasks in progress,
if(existingCallbackNode ! = =null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// The task priority does not change, indicating that the previous task can be reused together
return;
}
// The task priority has changed, indicating that the task cannot be reused.
// Cancel the executing task and reschedule it
cancelCallback(existingCallbackNode);
}
// Make a new schedule
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
// To synchronize the task priority, run performSyncWorkOnRoot
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncBatchedLanePriority) {
// If the task priority is batch synchronization, run performSyncWorkOnRoot
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else {
// ...
/ / if not batch synchronization task priority, execute performConcurrentWorkOnRoot
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// ...
}
Copy the code
“With the ensureRootIsScheduled method, the priority of the root node task is altered if a new one is added to the ensureRootIsScheduled method. If no one is altered, the new one will be executed with the current schedule. If there is a change, a new schedule is created and the performSyncWorkOnRoot(root) method is also called to start the synchronization task.
Perform interruptible updates
React when the type of task is not synchronous type will also perform ensureRootIsScheduled method, because is asynchronous task, will eventually perform performConcurrentWorkOnRoot method for interruptible updates, will be talked about in more detail below.
workLoop
synchronous
For example, performSyncWorkOnRoot goes through the following process: performSyncWorkOnRoot — > renderRootSync — > workLoopSync.
In workLoopSync, as long as workInProgress (the newly created fiber node in the workInProgress Fiber tree) is not null, the performUnitOfWork function will be executed.
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function workLoopSync() {
while(workInProgress ! = =null) { performUnitOfWork(workInProgress); }}Copy the code
interruptible
Interruptible mode, performConcurrentWorkOnRoot will perform the following process: performConcurrentWorkOnRoot – > renderRootConcurrent – > workLoopConcurrent.
In contrast to workLoopSync, workLoopConcurrent determines the following shouldYield() value before performing performUnitOfWork on each workInProgress. If it is false, execution continues; if it is true, execution is interrupted.
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function workLoopConcurrent() {
while(workInProgress ! = =null&&! shouldYield()) { performUnitOfWork(workInProgress); }}Copy the code
performUnitOfWork
Eventually, whether the task is executed synchronously or interruptible, it goes into the performUnitOfWork function.
Fiber will be used as a unit in FormUnitofwork to coordinate the process. The workIngProgress is updated after each beginWork execution, which responds to the workLoop above.
Until the fiber tree convenience is complete, workInProgress is set to null and the completeUnitOfWork function is executed.
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function performUnitOfWork(unitOfWork: Fiber) :void {
// ...
const current = unitOfWork.alternate;
// ...
let next;
if(enableProfilerTimer && (unitOfWork.mode & ProfileMode) ! == NoMode) {// ...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
// ...
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
Copy the code
beginWork
The beginWork function is called originalBeginWork based on the current execution environment:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
let beginWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
beginWork = (current, unitOfWork, lanes) = > {
// ...
try {
return originalBeginWork(current, unitOfWork, lanes);
} catch (originalError) {
// ...}}; }else {
beginWork = originalBeginWork;
}
Copy the code
OriginalBeginWork performs update functions for different types of React elements based on the workInProgress tag attribute. But the reconcileChildren function ends up calling the reconcileChildren function, regardless of the tag type.
// packages/react-reconciler/src/ReactFiberBeginWork.old.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) :Fiber | null {
const updateLanes = workInProgress.lanes;
workInProgress.lanes = NoLanes;
// Perform an update to the workInProgress tag
switch (workInProgress.tag) {
// ...
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
// ...
}
// ...
}
Copy the code
In the case of updateHostRoot, the mountChildFibers or reconcileChildren are executed according to whether the root fiber exists.
// packages/react-reconciler/src/ReactFiberBeginWork.old.js
function updateHostRoot(current, workInProgress, renderLanes) {
// ...
const root: FiberRoot = workInProgress.stateNode;
if (root.hydrate && enterHydrationState(workInProgress)) {
// If the root fiber is not present, this is the first rendering
// ...
const child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
workInProgress.child = child;
} else {
// If a fiber exists, call the reconcileChildren
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
resetHydrationState();
}
return workInProgress.child;
}
Copy the code
What Reconci Child does is one of the other cores of React — the Diff process, which will be covered in more detail in the next article.
completeUnitOfWork
When workInProgress is null, the completeUnitOfWork function is entered after the fiber tree of the current task is traversed.
After the beginWork operation, the workInProgress node has been flagged for side effects. The completeUnitOfWork method collects the effects chain layer by layer, eventually to root for the next commit phase.
After the completeUnitOfWork finishes, the Render phase ends, followed by the Commit phase.
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function completeUnitOfWork(unitOfWork: Fiber) :void {
let completedWork = unitOfWork;
do {
// ...
// Perform completeWork on the node, generate DOM, update props, bind events
next = completeWork(current, completedWork, subtreeRenderLanes);
if( returnFiber ! = =null &&
(returnFiber.flags & Incomplete) === NoFlags
) {
// Merge the current node's effectList into the parent node's effectList
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if(completedWork.lastEffect ! = =null) {
if(returnFiber.lastEffect ! = =null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// Add itself to the effectList chain, skip NoWork and PerformedWork flags when adding, because real commit is not used
const flags = completedWork.flags;
if (flags > PerformedWork) {
if(returnFiber.lastEffect ! = =null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else{ returnFiber.firstEffect = completedWork; } returnFiber.lastEffect = completedWork; }}}while(completedWork ! = =null);
// ...
}
Copy the code
scheduler
Realize frame idle scheduling task
The react update task will be executed at every idle moment of each frame. How does this work? It’s easy to think of an API — requestIdleCallback. However, react implemented a similar feature instead of requestIdleCallback due to compatibility issues with requestIdleCallback and the need for some high-priority react tasks to sacrifice some frames.
When performing interruptible update, we talked about above performConcurrentWorkOnRoot function through scheduleCallback package:
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
Copy the code
ScheduleCallback function refers to the packages/scheduler/SRC/scheduler. Js unstable_scheduleCallback function under the path, let’s take a look at the function, it will go as planned insert scheduling tasks:
// packages/scheduler/src/Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
// ...
if (startTime > currentTime) {
// The current task has timed out. The timeout queue is inserted
// ...
} else {
// The task has not timed out
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// Match the update schedule execution flag
if(! isHostCallbackScheduled && ! isPerformingWork) { isHostCallbackScheduled =true;
// requestHostCallback schedules the taskrequestHostCallback(flushWork); }}return newTask;
}
Copy the code
After the task is inserted into the scheduling queue, the requestHostCallback function is used to schedule the task.
React creates a MessageChannel through new MessageChannel(), which uses postMessage to inform the scheduler to start scheduling when the js thread is found idle. React then receives a notification of the start of the schedule and uses the performWorkUntilDeadline function to update the end time of the current frame and execute the task. Thus the task scheduling of frame idle time is realized.
// packages/scheduler/src/forks/SchedulerHostConfig.default.js
// Get the duration of each frame on the current device
forceFrameRate = function(fps) {
// ...
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5; }};// Execute the task before the frame ends
const performWorkUntilDeadline = () = > {
if(scheduledHostCallback ! = =null) {
const currentTime = getCurrentTime();
// Update the end time of the current frame
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
// If there are still scheduled tasks, execute them
if(! hasMoreWork) { isMessageLoopRunning =false;
scheduledHostCallback = null;
} else {
// The task is terminated with a postMessage notification
port.postMessage(null); }}catch (error) {
// ..
throwerror; }}else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
// create a MessageChannel through MessageChannel to implement task scheduling notification
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
PostMessage notifies the scheduler that frame scheduling has started
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if(! isMessageLoopRunning) { isMessageLoopRunning =true;
port.postMessage(null); }};Copy the code
A mission
For interruptible workLoop, shouYield is determined before performUnitOfWork is executed
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function workLoopConcurrent() {
while(workInProgress ! = =null&&! shouldYield()) { performUnitOfWork(workInProgress); }}Copy the code
Let’s look at how shouYield is obtained:
// packages\scheduler\src\SchedulerPostTask.js
export function unstable_shouldYield() {
return getCurrentTime() >= deadline;
}
Copy the code
GetCurrentTime retrieves the current timestamp, while Deadline says it is the end time of each frame in the browser. In concurrent mode, React puts these asynchronous tasks to be executed during the idle period of each browser frame. If the execution is not complete at the end of each frame, the current task is interrupted until the browser is idle in the next frame.
conclusion
Here’s a summary of the react Render phase design ideas:
- When a render or update operation occurs, React creates a series of tasks with priority, and then builds a linked list of workInProgress Fiber trees.
- Iterate through the task list to execute the task. Each frame performs tasks such as browser rendering first and, if the current frame has free time, until the time of the current frame runs out. If the current frame has no free time, wait until the next frame has free time to execute. If the current frame has no free time but the current task list has tasks that are due or have immediate execution tasks, those tasks are executed when they must be executed at the cost of losing several frames. The completed tasks are deleted from the linked list.
The flow chart in the execution process is as follows: