This is the 10th day of my participation in the August More Text Challenge. For details, see:August is more challenging
Before formally analyzing the Rendering of the Fiber tree, let’s once again review the four stages of the reconciler’s operational process:
- Input stage: connection
react-dom
Package,Fiber update
Request (refer toThe React app startup process). - Registering scheduling tasks: With the scheduling center (
scheduler
Package) to register for scheduling taskstask
, waiting for a task callback (seeReact Scheduler Scheduler). - Execution task callback: constructed in memory
Fiber tree
andDOM
Object (reference)Fiber tree construct (first created)And the Fiber tree (contrast update)). - Output: With the renderer (
react-dom
) interaction, renderingDOM
Node.
In the fourth stage (output) analyzed in this section, the Fiber tree rendering is important because it is the last step in the pipeline of reconciler operations, or because all previous steps serve the last step.
Now that I’ve introduced the fiber tree structure, I’m going to analyze the fiber tree rendering process, which is actually a further processing of the fiber tree.
Fiber tree characteristics
Through the above interpretation of fiber tree structure, the basic characteristics of fiber tree can be summarized:
- Whether it is
For the first time to construct
Or is itCompared to update
, which will eventually generate a tree in memory for rendering the pageFiber tree
(i.e.fiberRoot.finishedWork
). - This one is going to be rendered
Fiber tree
There are two characteristics:- The side effect queue is mounted on the root node (specifically
finishedWork.firstEffect
) - Representing the latest page
DOM
The object is mounted onFiber tree
In the firstHostComponent
Type of node (specificallyDOM
Object is mounted onfiber.stateNode
Attributes)
- The side effect queue is mounted on the root node (specifically
Once again, the two fiber trees used in the previous article can verify the above characteristics:
- Primary structural
- Compared to update
commitRoot
The entire rendering logic is in the commitRoot function:
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
Copy the code
Both render and scheduling priorities are used in commitRoot. The discussion of priorities has already been explained earlier (see Priority management in React and Fiber tree construction (foundation preparation)# priorities) and will not be discussed in this section. The final implementation is through the commitRootImpl function:
/ /... Omit some irrelevant code
function commitRootImpl(root, renderPriorityLevel) {
// ============ Before rendering: prepare ============
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// Clear the FiberRoot object properties
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
if (root === workInProgressRoot) {
// Reset the global variable
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// Update the side effect queue again
let firstEffect;
if (finishedWork.flags > PerformedWork) {
// By default, the fiber node's side effect queue does not include itself
// If the root node has side effects, add the root node to the end of the side effects queue
if(finishedWork.lastEffect ! = =null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else{ firstEffect = finishedWork; }}else {
firstEffect = finishedWork.firstEffect;
}
// ============ render ============
let firstEffect = finishedWork.firstEffect;
if(firstEffect ! = =null) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// Stage 1: before the DOM mutation
nextEffect = firstEffect;
do {
commitBeforeMutationEffects();
} while(nextEffect ! = =null);
// Stage 2: DOM mutation, interface change
nextEffect = firstEffect;
do {
commitMutationEffects(root, renderPriorityLevel);
} while(nextEffect ! = =null);
// Restore the interface status
resetAfterCommit(root.containerInfo);
// Switch the current pointer
root.current = finishedWork;
// Phase 3: Layout phase, calling life cycle componentDidUpdate and callback functions
nextEffect = firstEffect;
do {
commitLayoutEffects(root, lanes);
} while(nextEffect ! = =null);
nextEffect = null;
executionContext = prevExecutionContext;
}
// ============ after rendering: reset and clean ============
if (rootDoesHavePassiveEffects) {
// Have passive action (useEffect), save some global variables
} else {
// Decompose the list of side effect queues to assist garbage collection
// If there is a passive action (useEffect), put the decomposition in the flushPassiveEffects function
nextEffect = firstEffect;
while(nextEffect ! = =null) {
const nextNextEffect = nextEffect.nextEffect;
nextEffect.nextEffect = null;
if(nextEffect.flags & Deletion) { detachFiberAfterEffects(nextEffect); } nextEffect = nextNextEffect; }}// Reset some global variables (omitted)...
// The following code is used to check if there is a new update task
// For example, in componentDidMount, call setState() again
// 1. Detect routine (asynchronous) tasks, and initiate asynchronous scheduling (scheduler 'can only be called asynchronously)
ensureRootIsScheduled(root, now());
// 2. Check for a synchronization task and call flushSyncCallbackQueue(no need to wait for scheduler to schedule again) to enter the fiber tree construction loop again
flushSyncCallbackQueue();
return null;
}
Copy the code
The commitRootImpl function divides the entire commitRootImpl into three segments (before, render, and after), depending on whether rendering is called.
Before rendering
Do some preparatory work for the next formal rendering. Mainly include:
- Set global status (for example, update)
fiberRoot
Properties on - Reset global variables (e.g.
workInProgressRoot
.workInProgress
Etc.) - Update the side effect queue again: for the root node only
fiberRoot.finishedWork
- By default, the root node’s side effect queue does not include itself. If the root node has side effects, the root node is added to the end of the side effect queue
- Notice it just lengthens the side effect queue, but
fiberRoot.lastEffect
The pointer doesn’t change. For example, when first constructed, the root node hasSnapshot
Tags:
Apply colours to a drawing
The main logic in the rendering phase of the commitRootImpl function is to process the side effect queue and render the latest DOM node (already in memory but not yet rendered) to the interface.
The whole rendering process is distributed into 3 functions:
commitBeforeMutationEffects
- Dom changes before processing side effects queue with
Snapshot
.Passive
Of the tagfiber
Node.
- Dom changes before processing side effects queue with
commitMutationEffects
- The DOM changes and the interface is updated. Processing side effects queue with
Placement
.Update
.Deletion
.Hydrating
Of the tagfiber
Node.
- The DOM changes and the interface is updated. Processing side effects queue with
commitLayoutEffects
- After dom changes, handle side effects with
Update | Callback
Of the tagfiber
Node.
- After dom changes, handle side effects with
From the above source code analysis, commitRootImpl’s responsibilities can be summarized into two areas:
- Process the side effect queue. (Step 1, step 2 and Step 3 are both processed, only the node id is processed
fiber.flags
Different). - Call the renderer and print the final result (in Step 2:
commitMutationEffects
Performed).
CommitRootImpl processes fiberRoot. FinishedWork the fiber tree that is about to be rendered. In theory, it is not necessary to care how the fiber tree is generated (either by first construction or by contrast update). For clarity and simplicity, all the diagrams below use the fiber tree structure created the first time.
The objects that these three functions deal with are side effect queues and DOM objects.
No matter how complex the fiber tree structure is, at commitRoot stage, only two nodes actually work:
Side effect queue
Node: The root node, that isHostRootFiber
Node.DOM object
Node: First node from top to bottomHostComponent
The type offiber
Node, this nodefiber.stateNode
It actually points to the latest DOM tree.
For clarity, we omit some of the irrelevant references, leaving only the fiber nodes that are actually used in the commitRoot phase:
commitBeforeMutationEffects
Phase 1: Process the Fiber node with Snapshot,Passive tags in the side effect queue before dom changes.
/ /... Omit some irrelevant code
function commitBeforeMutationEffects() {
while(nextEffect ! = =null) {
const current = nextEffect.alternate;
const flags = nextEffect.flags;
// Process the 'Snapshot' tag
if((flags & Snapshot) ! == NoFlags) { commitBeforeMutationEffectOnFiber(current, nextEffect); }// Process the 'Passive' flag
if((flags & Passive) ! == NoFlags) {// The Passive flag only appears when hook is used. So here is the processing of the hook object
if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
scheduleCallback(NormalSchedulerPriority, () = > {
flushPassiveEffects();
return null; }); } } nextEffect = nextEffect.nextEffect; }}Copy the code
- To deal with
Snapshot
tag
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
) :void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if(current ! = =null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
constsnapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); instance.__reactInternalSnapshotBeforeUpdate = snapshot; }}return;
}
case HostRoot: {
if (supportsMutation) {
if (finishedWork.flags & Snapshot) {
constroot = finishedWork.stateNode; clearContainer(root.containerInfo); }}return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
return; }}Copy the code
As you can see from the source code, the only types associated with the Snapshot tag are ClassComponent and HostRoot.
- for
ClassComponent
Type node, calledinstance.getSnapshotBeforeUpdate
Life cycle function - for
HostRoot
Type node, callclearContainer
Empty the container node (i.ediv#root
The DOM node).
- To deal with
Passive
tag
The Passive flag exists only on function nodes that use hook objects. For details about the subsequent operations, see hook principles. Here we need to know that in the first phase of commitRoot, in order to process hook objects (such as useEffect), a scheduler task is registered separately through scheduleCallback, waiting for the scheduler to process.
Note: All tasks scheduled through the Scheduler are triggered by MessageChannel and are asynchronously executed (see React Scheduler).
Test:
// In the following example code, the output order is 1, 3, 4, 2
function Test() {
console.log(1);
useEffect(() = > {
console.log(2);
});
console.log(3);
Promise.resolve(() = > {
console.log(4);
});
return <div>test</div>;
}
Copy the code
commitMutationEffects
Phase 2: THE DOM changes and the interface is updated. Processes fiber nodes in the side effect queue with ContentReset, Ref, Placement, Update, Deletion, Hydrating tags.
/ /... Omit some irrelevant code
function commitMutationEffects(root: FiberRoot, renderPriorityLevel: ReactPriorityLevel,) {
/ / deal with Ref
if (flags & Ref) {
const current = nextEffect.alternate;
if(current ! = =null) {
// Clear the ref (commitRoot) and reassign the value after the dom changecommitDetachRef(current); }}// Handle DOM mutations
while(nextEffect ! = =null) {
const flags = nextEffect.flags;
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement: {
// Add a new node
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement; // Note that the Placement marker will be removed
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
// Update the node
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
// Delete the node
commitDeletion(root, nextEffect, renderPriorityLevel);
break; } } nextEffect = nextEffect.nextEffect; }}Copy the code
Dealing with DOM mutations:
new
: Function call stackcommitPlacement
->insertOrAppendPlacementNode
->appendChild
update
: Function call stackcommitWork
->commitUpdate
delete
: Function call stackcommitDeletion
->removeChild
AppendChild, commitUpdate, removeChild functions in the React-DOM package are called. They are standard functions specified in the HostConfig protocol (source code in ReactDomHostconfig.js) and implemented in the React-dom renderer package. These functions operate directly on the DOM, so the interface is updated after execution.
Note: After commitMutationEffects executes, switch the current Fiber tree (root.current = finishedWork) in the commitRootImpl function, ensuring that fiberroot.current points to the fiber tree representing the current interface.
commitLayoutEffects
Phase 3: After dom changes, process the fiber node in the side effect queue with Update, Callback, and Ref tags.
/ /... Omit some irrelevant code
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while(nextEffect ! = =null) {
const flags = nextEffect.flags;
// Handle the Update and Callback tags
if (flags & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (flags & Ref) {
// Reset the refcommitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code
Core logic are commitLayoutEffectOnFiber – > commitLifeCycles function.
/ /... Omit some irrelevant code
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
) :void {
switch (finishedWork.tag) {
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// First render: call componentDidMount
instance.componentDidMount();
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
// Update phase: call componentDidUpdateinstance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); }}const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if(updateQueue ! = =null) {
This.setstate ({}, callback)
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
// Set the native state such as focus
commitMount(instance, type, props, finishedWork);
}
return; }}}Copy the code
In the commitLifeCycles function:
- for
ClassComponent
Node, calling the lifecycle functioncomponentDidMount
orcomponentDidUpdate
, the callupdate.callback
Callback function. - for
HostComponent
Node, if anyUpdate
Flag, some native states need to be set (e.g.focus
Etc.)
After the rendering
After performing the above steps, the rendering task is complete. After rendering is complete, some resets and cleanups are needed:
-
Clear the side effect queue
- Because the side effect queue is a linked list, because the single
fiber
Object reference, cannot beThe gc recycling
. - Take the list apart, when
fiber
When an object is no longer in use, it can beThe gc recycling
.
- Because the side effect queue is a linked list, because the single
2. Check for updates
- Throughout the rendering process, it is possible to generate new
update
(as incomponentDidMount
Function, called againsetState()
). - If it is a regular (asynchronous) task, no special treatment, call
ensureRootIsScheduled
Ensure that the task has been registered with the scheduling center. - If it is a synchronous task, it is invoked actively
flushSyncCallbackQueue
(without waiting again for scheduler scheduling), enter the fiber tree construction loop again
// Clear the side effect queue
if (rootDoesHavePassiveEffects) {
// Have passive action (useEffect), save some global variables
} else {
// Decompose the list of side effect queues to assist garbage collection.
// If there is a passive action (useEffect), put the decomposition in the flushPassiveEffects function
nextEffect = firstEffect;
while(nextEffect ! = =null) {
const nextNextEffect = nextEffect.nextEffect;
nextEffect.nextEffect = null;
if(nextEffect.flags & Deletion) { detachFiberAfterEffects(nextEffect); } nextEffect = nextNextEffect; }}// Reset some global variables (omitted)...
// The following code is used to check if there is a new update task
// For example, in componentDidMount, call setState() again
// 1. Detect routine (asynchronous) tasks, and initiate asynchronous scheduling (scheduler 'can only be called asynchronously)
ensureRootIsScheduled(root, now());
// 2. Check for a synchronization task and call flushSyncCallbackQueue(no need to wait for scheduler to schedule again) to enter the fiber tree construction loop again
flushSyncCallbackQueue();
Copy the code
conclusion
This section analyzes the processing process of Fiber tree rendering. From a macro perspective, fiber tree rendering is located in the output stage of reconciler operation process, and is the last link (from input to output) in the entire reconciler operation process. In this section, the commitRootImpl function is decomposed from three aspects: before rendering, after rendering, according to the source code. The core rendering logic is divided into three functions, which together handle the fiber node with side effects and render the latest DOM objects to the interface through the react-DOM renderer.
Write in the last
This article belongs to the diagram react source code series in the operation of the core plate, this series of nearly 20 articles, really in order to understand the React source code, and then improve the architecture and coding ability.
The first draft of the graphic section has been completed and will be updated in August. If there are any errors in the article, we will correct them as soon as possible on Github.