Series article directory (synchronous update)
- React source code analysis series – render phase of react (I): introduction to the basic process
- React source code analysis series – render phase of react (II): beginwork
- React source code analysis series – render phase of react (III): completeunitofwork
This series is all about react V17 0.0-alpha source code
performUnitOfWork
memoryReact source code analysis series – render phase of react (I): introduction to basic processIntroduced inperformUnitOfWorkmethod:
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork. alternate; // The corresponding fiber node on the current tree may be null
// ... ellipsis
let next; // Used to store the results returned by beginwork()
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// ... ellipsis
unitOfWork.memoizedProps = unitOfWork.pendingProps;
If (next = = = null) {// beginwork returns null, indicating that there is no (or no need to pay attention to) the child fiber node of the current node
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next; // The main body of the while loop of the next workloopsync / workloopconcurrent is the child fiber node
}
// ... ellipsis
}
As the “return” phase of render, it can only be executed after the “delivery” phase of render is completed; In other words, when beginwork returns a null value, that isThe current node does not have (or need not pay attention to) the child fiber node of the current nodeWill enter the “return” stage of render——completeUnitOfWork 。
completeUnitOfWork
Let’s look at the protagonist of this article——completeUnitOfWorkmethod:
function completeUnitOfWork(unitOfWork: Fiber): void {
/*
Complete some processing of the current fiber node
After processing, if the current node still has a sibling node, end the current method and enter the next performnunitofwork loop
If there is no sibling node, the parent node (completedwork. Return) will be returned,
Until the parent node is null, it indicates that the whole workinprogress fiber tree has been processed.
*/
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.effectTag & Incomplete) === NoEffect) {
let next;
// ... ellipsis
next = completeWork(current, completedWork, subtreeRenderLanes);
// ... ellipsis
/*
If the return of completework is not empty, it will enter the next performnunitofwork loop
However, this situation is too rare. At present, I only see that there will be a return related to suspend, so I think this code segment will not be executed for the time being
*/
if (next !== null) {
workInProgress = next;
return;
}
// ... ellipsis
if (
returnFiber !== null &&
(returnFiber.effectTag & Incomplete) === NoEffect
) {
/*Collect all child fiber nodes with effecttag and mount them on the current node in the form of a linked list (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;
}
/*If the current fiber node (completedwork) also has an effecttag, put it after the child fiber node (in the effectlist)*/
const effectTag = completedWork.effectTag;
/*Skip the nodes of nowork and performedwork effecttag. There is no need to explain nowork. Performedwork is for devtools*/
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
//Exception handling, omitting
}
//Take the sibling node of the current fiber node (completedwork);
//If there is a value, the completeunitofwork is ended, and the sibling node is used as the main body (unitofwork) of the next performnunitofwork
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
//If there is no sibling node, the next do Process the parent node in the while loop (completedwork. Return)
completedWork = returnFiber;
//Note here!
//Although workinprogress is set to completedwork, it is meaningless because there is no return, that is, it does not end completeunitofwork
//Until completedwork (actually completedwork. Return in this loop) is null, end do After the while loop
//At this time, the running result (workinprogress) of completeunitofwork is null
//It also means that the while loop in performsyncworkonroot / performcurrentworkonroot has reached the end condition
workInProgress = completedWork;
} while (completedWork !== null);
//Omit
}
Please see the flow chart:
According to the flow chart,completeUnitOfWorkTwo main things have been done: implementationcompleteWorkandCollapse effectlist, let’s introduce these two parts in detail.
completeWork
If the beginwork method in the “delivery” stage is mainly to create child nodes, then the beginwork method in the “return” stagecompleteWorkThe main method is to create the DOM node of the current node and collapse the DOM node and effectlist of the child node.
Similar to beginwork,completeWorkThe current node will also be differenttagTypes perform different logic:
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ... ellipsis
return null;
}
case HostRoot: {
// ... ellipsis
return null;
}
case HostComponent: {
// ... ellipsis
return null;
}
// ... ellipsis
}
It should be noted that many types of nodes do not have the logic of completework (that is, they are directly connected without doing anything)return null
)For example, very commonFragmentandFunctionComponent。 We focus on what is necessary for page renderingHostComponent, i.eHTML tags(e.g<div></div>
)Fiber node converted to.
Process hostcomponent
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
// ... ellipsis
case HostComponent: {
// ... ellipsis
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
// ... ellipsis
} else {
// ... ellipsis
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
return null;
}
// ... ellipsis
}
}
From the above code segment, we can know that the processing of hostcomponent by completework method mainly has two code branches:
(current !== null && workInProgress.stateNode != null) === true
The current node is modified“to update”Operation;(current !== null && workInProgress.stateNode != null) === true
The current node is modified“newly build”Operation;
The reason why mount (first screen rendering) and update commonly used in previous articles are not used here is that there is a situation that iscurrent !== null
andworkInProgress.stateNode === null
: during update, if the current fiber node is a new node and has been marked with placement effecttag in the beginwork phase, then the statenode will be null; In this case, the same needs to be done“newly build”Operation.
Update operation for hostcomponent
In this code branch, because it has been determinedworkInProgress.stateNode !== null
, that is, the corresponding DOM node already exists, so there is no need to generate DOM nodes.
We can see that this is mainly the implementation of oneupdateHostComponentmethod:
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
/*If props does not change (the current node is reused through the bailoutonalreadyfinished work method), you can skip the processing of the current node*/
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
//Omit
/*Calculate the DOM node attributes that need to be changed and store them in an array (the elements of the even index of the array are the attribute names, and the elements of the cardinality index of the array are the attribute values)*/
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
//Mount the calculated updatepapayload in workinprogress On updatequeue for subsequent commit phases
workInProgress.updateQueue = (updatePayload: any);
//If updatepayload is not empty, update's effecttag will be marked on the current node
if (updatePayload) {
markUpdate(workInProgress);
}
};
As you can see from the code snippet aboveupdateHostComponentThe main function of is to calculate the DOM node attributes that need to be changed and mark the current node with the update effecttag.
prepareUpdate
Next let’s seeprepareUpdateHow to calculate the DOM node attributes that need to be changed:
export function prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): null | Array<mixed> {
//Omit dev code
return diffProperties(
domElement,
type,
oldProps,
newProps,
rootContainerInstance,
);
}
It can be seen thatprepareUpdateIn fact, the diffproperties method is called directly.
diffProperties
diffPropertiesThere are a lot of methods. I won’t put the source code here. Let’s talk about the process:
- Do special processing for lastprops & nextprops of a specific tag (because this scenario deals with hostcomponent, so tag is the HTML tag name), including input / select / textarea. For example, the value value of input may be a number, while the value of native input only accepts string, so the data type needs to be converted.
-
Traverse lastprops:
- If the prop also exists in nextprops, skip it, which means that the prop has not changed and does not need to be processed.
- When you see the prop with style, it is sorted into the styleupdates variable (object), and this part of the style attribute is set to null value
- Push the propkey other than the above into an array (updatepapayload), and then push a null value into the array, which means that the prop is cleared.
-
Traverse nextprops:
- If the nextprop is consistent with lastprop, that is, there is no change before and after the update, skip.
- When you see prop with style, sort it into the styleupdates variable. Note that this part of the style attribute has value
- Handling dangerously_ SET_ INNER_ HTML
- Dealing with children
- In addition to the above scenarios, the key and value of prop are directly pushed into the array (updatepapayload).
- If styleupdates is not empty, push the ‘style’ and styleupdates variables into the array (updatepapayload).
- Returns updatepapayload.
updatePayloadIs an array in which the elements of the even index of the array areprop key
, the element of array cardinality index isprop value
。
markUpdate
Let’s go onmarkUpdateMethod, the method is actually very simple, that is, inworkInProgress.effectTag
Hit aUpdate EffectTag
。
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.effectTag |= Update;
}
New operation for hostcomponent
The main logic of “new” operation includes three aspects:
- Generate the corresponding DOM node for the fiber node: createinstance method
- Insert the descendant DOM node into the newly generated DOM node: appendallchildren method
- Initialize all properties of the current DOM node and event callback processing: finalizeinitialchildren method
createInstance
Let’s see“Generate the corresponding DOM node for the fiber node”Method of——createInstance :
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
//Omit dev snippet
//Determine the namespace (xmlns attribute) of the DOM node, generally“ http://www.w3.org/1999/xhtml "
parentNamespace = ((hostContext: any): HostContextProd);
//Create DOM element
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
//Create an attribute (pointer) pointing to the fiber node object on the DOM object to facilitate subsequent access
precacheFiberNode(internalInstanceHandle, domElement);
//Create an attribute (pointer) pointing to props on the DOM object to facilitate subsequent access
updateFiberProps(domElement, props);
return domElement;
}
It can be seen that createinstance mainly calls the createElement method to create DOM elements; As for createElement, this article will not expand. If you are interested, you can have a lookSource code。
appendAllChildren
Let’s look at the method of “inserting the descendant DOM node into the newly generated DOM node”——appendAllChildren :
//Completework is called as follows: appendallchildren (instance, workinprogress, false, false);
appendAllChildren = function(
Parent: instance, // relative to the child node to append, the node currently processed by completework is the parent node
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
let node = workInProgress. child; // First sub fiber node
/*This while loop is essentially a depth first traversal*/
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
//If it is a child node corresponding to HTML tag or plain text, add the dom of the current child node to the end of the DOM child node list of the parent node
appendInitialChild(parent, node.stateNode);
}Else if (enablefundamentalapi & & node. Tag = = = fundamentalcomponent) {// ignore first
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {
// ... No operation
} else if (node.child !== null) {
//For some special types of child nodes, such as < fragment / >, try to get DOM from the child nodes of the child nodes
node. child. return = node; // Set the return pointer to facilitate subsequent identification of whether the loop end condition is met
node = node. child; // The loop body changes from a child node to a child node of a child node
continue; // Start a new cycle immediately
}
if (node === workInProgress) {
return; // When traversing "regression", it is found that the end condition of traversal has been reached, and the traversal is ended
}
//If the current circular principal node has no siblings, perform "regression"; And if it is found that there is still no sibling after the "regression", the "regression" will continue
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return; // When the end condition of traversal is reached in the process of "regression", the traversal is ended
}
node = node. return; // Result of "regression": add node Return is the subject of the next cycle
}
//This indicates that the current loop body has sibling
node. sibling. return = node. return; // Set the return pointer to facilitate subsequent identification of whether the loop end condition is met
node = node. sibling; // Add node Sibling is the body of the next cycle
}
};
//Appendinitialchild essentially executes the native DOM node method appendChild
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void {
parentInstance.appendChild(child);
}
appendAllChildrenIt is essentially a depth first traversal with conditional constraints (limiting progressive levels):
- Take the first child node of the current node (parent) as the loop body(
node
)。 - If the loop body is
Fiber node corresponding to HTML tag or plain text
, the DOMappendChild
toparent
。 - If the current loop body(
node
)Have sibling nodes(node.sibling
), set the sibling node as the main body of the next cycle.
Just looking at the above process, isn’t it a typical breadth first traversal? Don’t worry, because there is another special case: when the current cycle subject is notFiber node corresponding to HTML tag or plain text
, and the current cycle body has child nodes(node.child
)When, the child node of the current cycle body is taken as the body of the next cycle, and the next cycle starts immediately(continue
)。
Take the following component as an example:
function App() {
return (
<div>
<b>1</b>
<Fragment>
<span>2</span>
<p>3</p>
</Fragment>
</div>
)
}
according toReact source code analysis series – render phase of react (I): introduction to basic processThe execution sequence of beginwork and completework in can be obtained:
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. b Fiber beginWork
5. B Fiber completework // current node - < B / >, appendChild text node
6. Fragment Fiber beginWork
7. span Fiber beginWork
8. Span fiber completework // current node - < span / >, appendChild text node
9. p Fiber beginWork
10. P fiber completework // current node - < P / >, appendChild text node
11. Fragment fiber completework // skip
12. Div fiber completework // let's focus on this one
13. App Fiber completeWork
14. rootFiber completeWork
Let’s focus on the div nodeappendAllChildren
:
- Initialization before executing the while loop: take out the first child node of div node – node B as the body of the first while loop.
-
The first while loop (the main body of the loop is node b):
- Node B is a hostcomponent, which directly appendChild.
- B node has a sibling node, that is, the fragment node, which is set as the body of the next while loop(
node
)。
-
The second while loop (the main body of the loop is the fragment node):
- Since the fragment node is neither hostcomponent nor hosttext, the span node, the first child node of the fragment node, will be taken as the body of the next while loop(
node
)。 - Enter immediately(
continue
)The next while loop.
- Since the fragment node is neither hostcomponent nor hosttext, the span node, the first child node of the fragment node, will be taken as the body of the next while loop(
-
The third while loop (the main body of the loop is the span node):
- The span node is a hostcomponent and directly appendChild.
- Span node has a sibling node, P node, which is set as the body of the next while loop(
node
)。
-
The fourth while loop (the main body of the loop is the P node):
- The P node is a hostcomponent and directly appendChild.
- The P node has no sibling nodes, so regression is performed(
node = node.return
)At this point, in this “regression” snippet — ASmall while loop
In, the loop body becomes the parent node of the P node, that is, the fragment node. - Continue next time
Small while loop
: because the fragment has no sibling nodes, it is not satisfiedSmall while loop
Therefore, continue the “regression”, and at this time, the main body of the cycle(node
)Is a div node. - Continue next time
Small while loop
: because div node meetsnode.return === workInProgress
Therefore, directly end the whole traversal process – appendallchildren.
finalizeInitialChildren
Let’s look at “initializing all properties of the current DOM node and event callback processing”——finalizeInitialChildren :
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}
From the above code segment, we can clearly see that finalizeinitialchildren is mainly divided into two steps:
- implementsetInitialPropertiesmethod; Note that this method is different from prepareupdate. This method will actually mount the DOM attribute to the DOM node and call itaddEventListenerBind the event processing callback to the current DOM node.
- implementshouldAutoFocusHostComponentMethod: Return
props.autoFocus
Value of (only)button
/input
/select
/textarea
Support).
Collapse effectlist
As the basis of DOM operation, the commit phase needs to find all fiber nodes with effecttag and perform the corresponding operations of effecttag in turn. Do you still need to traverse the fiber tree again in the commit phase? This is obviously inefficient.
In order to solve this problem, in completeunitofwork, each fiber node that executes completework and has an effecttag will be saved in a one-way linked list called effectlist; The first fiber node in the effectlist is saved in fiber Firsteffect, the last element is saved in fiber lastEffect 。
The parent node of “allegist” will be added in the “allegist. Child”, which is similar to the parent node of “allegist” One way linked list with firsteffect as the starting point.
If the current fiber node (completedwork) also has an effecttag, put it after the child fiber node (in the effectlist).
/*If the head pointer of the effectlist of the parent node is null, then directly assign the head pointer of the effectlist of this node to the head pointer of the parent node, which is equivalent to hanging the entire effectlist of this node directly in the parent node*/
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
/*If the effectlist of the parent node is not empty, mount the effectlist of this node after the effectlist of the parent node*/
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
/*If the current fiber node (completedwork) also has an effecttag, put it after the child fiber node (in the effectlist)*/
const effectTag = completedWork.effectTag;
/*Skip the nodes of nowork and performedwork effecttag. There is no need to explain nowork. Performedwork is for devtools*/
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
Completeunitofwork end
Completeunitofwork has two end scenarios:
- Current node(
completed
)Have sibling nodes(completed.sibling
)At this time, workinprogress (i.e. the loop body of performinutofwork) will be set as the brother node, and then the completeunitofwork method will be ended. After that, the next performinutofwork will be carried out, in other words, the “delivery” phase of the “brother node” – beginwork will be executed. - During the “return” of completeunitofwork,
completed
The value of isnull
, that is, the regression of the whole fiber tree has been completed; At this time, the value of workinprogress is null, which means that the while loop in workloopsync / workloopconcurrent method has also reached the end condition; So far, react’srenderEnd of phase.
When the render phase ends, theperformSyncWorkOnRootMethod is calledcommitRoot(root)
(here)root
Parameter passing refers to fiberrootnode) to start reactcommitWork in this stage.