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
beginWork
in the next loop cycle.If there is no sibling, it moves up to the parent node’s
completeWork
in 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; }