Every Fiber node that goes through beginWork will eventually reach completeWork. When beginWork has progressed to the point where it cannot go any deeper—in other words, when the current node has no child nodes—it will enter completeWork, as shown below:
function performUnitOfWork(unitOfWork) { // The current workInProgress node
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) { // When there are no child nodes, we’ve reached the end
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}completeUnitOfWork Traverses Sibling and Parent Nodes
completeUnitOfWork first checks for sibling nodes:
If there is a sibling, it moves to that sibling’s
beginWorkin the next loop cycle.If there is no sibling, it moves up to the parent node’s
completeWorkin the current loop.
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork; // This is the fiber node without children
do {
var current = completedWork.alternate;
var returnFiber = completedWork.return;
var next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) { // Normally, `next` returns null
workInProgress = next;
return;
}
var siblingFiber = completedWork.sibling;
if (siblingFiber !== null) { // If there’s a sibling, enter its `beginWork`
workInProgress = siblingFiber;
return;
}
// If no sibling, move up to the parent’s `completeWork`
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null); // Until we reach the root
}What Does completeWork Do?
The completeWork function handles different scenarios based on the fiber type:
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps;
// Enter different logic branches based on the type
switch(workInProgress.tag) {
case IndeterminateComponent:
case FunctionComponent:
...
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
...
bubbleProperties(workInProgress);
return null;
}
case HostRoot: {
var fiberRoot = workInProgress.stateNode;
popHostContainer(workInProgress);
updateHostContainer(current, workInProgress);
bubbleProperties(workInProgress);
return null;
}
...
}
}completeWork processes different branches based on the fiber type, handling distinct cases. Let’s examine two scenarios: initialization and updates.
Initialization
HostComponent
The first node to enter completeWork is typically a Fiber with no children, usually a HostComponent Fiber type. When it enters completeWork, it triggers the following logic:
// Creates an instance node, which is a real DOM object for HostComponent
var instance = createInstance(
type, // div
newProps, // { onClick, children, id, className ... }
rootContainerInstance, // #root
currentHostContext, // Not important here
workInProgress // Fiber#div
);
// Adds its child DOM nodes to its children list, touching the real DOM
appendAllChildren(instance, workInProgress, false, false);
// Adds the instance to the Fiber’s stateNode
workInProgress.stateNode = instance;
// Handles properties
bubbleProperties(workInProgress);appendAllChildren iterates through the fiber node’s children and adds their stateNode properties to instance, resulting in a fully constructed off-screen DOM tree by the time it reaches the root node. This bottom-up approach allows completeWork to merge from the leaf nodes into a complete DOM tree in memory.
Next, bubbleProperties collects each type of fiber flag, which is crucial for the subsequent commit phase:
function bubbleProperties(completedWork) {
var didBailout = completedWork.alternate !== null && completedWork.alternate.child === completedWork.child;
var newChildLanes = NoLanes;
var subtreeFlags = NoFlags;
if (!didBailout) { // Initialization
var _child = completedWork.child;
// Collect flags and lanes from direct children, assigning them to `childLanes` and `subtreeFlags` for the workInProgress
while (_child !== null) {
newChildLanes = mergeLanes(newChildLanes, mergeLanes(_child.lanes, _child.childLanes));
subtreeFlags |= _child.subtreeFlags;
subtreeFlags |= _child.flags;
_child.return = completedWork;
_child = _child.sibling;
}
completedWork.subtreeFlags |= subtreeFlags; // Merge subtree effects
} else { // Update
...
}
completedWork.childLanes = newChildLanes;
return didBailout;
}Each fiber node gathers all tags from its child nodes. These tags represent the operations to be performed on the actual DOM in the commit phase. Here are some flag types representing various effects in React:
export const NoFlags = /* */ 0b00000000000000000000000000; // No side effects export const PerformedWork = /* */ 0b00000000000000000000000001; // Related to devTools export const Placement = /* */ 0b00000000000000000000000010; // Insert export const Update = /* */ 0b00000000000000000000000100; // Update export const Deletion = /* */ 0b00000000000000000000001000; // Delete export const ChildDeletion = /* */ 0b00000000000000000000010000; // Delete child node export const ContentReset = /* */ 0b00000000000000000000100000; // Reset content export const Callback = /* */ 0b00000000000000000001000000; // Callback function // And many more… export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot | StoreConsistency; // Lifecycle side effects
In both beginWork and completeWork, side effect flags are marked on the current fiber node to dictate the specific DOM or side effect operations during the commit phase.
HostRoot
During initialization, when entering HostRoot, the following process occurs:
// Simplified example workInProgress.flags |= Snapshot; bubbleProperties(workInProgress);
The HostRoot receives a Snapshot flag, which will clear the previous UI during the commit phase by setting root.textContent = ''. Once completeWork reaches HostRoot, it signals the end of the initialization render process, after which updates can begin.
Update Phase
During the update phase, the first node encountered is the HostComponent, following this logic:
if (current !== null && workInProgress.stateNode != null) { // During updates
updateHostComponent$1( // If there’s an update, tag it
current,
workInProgress,
type,
newProps,
rootContainerInstance
);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress); // Tag it
}
} else {
// Initialization...
}
bubbleProperties(workInProgress);For HostComponent, the main focus during the update phase is not to create DOM nodes but to detect any updates. Within updateHostComponent$1, the diffProperties() function checks for differences between the new and old props. If differences are found, an Update tag is applied.
updateHostComponent$1 = function (
current,
workInProgress,
type,
newProps, // New props
rootContainerInstance // Current DOM object
) {
var oldProps = current.memoizedProps;
if (oldProps === newProps) { // No tag if props are identical
return;
}
var instance = workInProgress.stateNode;
var currentHostContext = getHostContext();
var updatePayload = prepareUpdate( // Check for differences
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext
);
workInProgress.updateQueue = updatePayload;
if (updatePayload) { // Apply update tag
markUpdate(workInProgress);
}
};This exemplifies the main behavior for HostComponent during the update phase. The bubbleProperties function in this phase continues to collect flags from its subtree.
Summary:
To conclude, here are the main responsibilities of completeWork:
Create DOM nodes
Apply tags
Collect child tags
Build an offscreen DOM tree
Once completeWork completes the last Fiber node, it marks the end of the render phase. At the end of completeUnitOfWork, workInProgressRootExitStatus is set to RootCompleted, signaling that the Fiber tree construction for this render phase is complete.
function completeUnitOfWork(unitOfWork){
var completedWork = unitOfWork;
do {
...
} while (completedWork !== null);
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}Finally, in renderRootSync or renderRootConcurrent, the workInProgressRootExitStatus is returned to the parent layer, which then determines the next actions based on the constructed Fiber tree’s state. If the status is RootCompleted, the workInProgress tree is assigned to root.finishedWork, leading into the commit phase.
function performSyncWorkOnRoot(root) {
...
var exitStatus = renderRootSync(root, lanes); // Render phase
if (exitStatus === RootFatalErrored) { // Incomplete case
...
throw fatalError;
}
if (exitStatus === RootDidNotComplete) { // Incomplete case
throw new Error("Root did not complete. This is a bug in React.");
}
var finishedWork = root.current.alternate; // The `workInProgress` tree
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// Enter commit phase
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions
);
// Schedule the next update
ensureRootIsScheduled(root, now());
return null;
}