react source code analysis 10.commit phase
Video Explanation (efficient learning): Enter learning
Previous articles:
1. Introduction and interview questions
3.react source code architecture
4. Source directory structure and debugging
6.legacy and concurrent mode entry functions
20. Summary & answers to interview questions in Chapter 1
commitRoot(root) will be called at the end of the render phase; Enter the commit phase, where root refers to fiberRoot, and then traverse the effectList generated in the render phase. The Fiber node on the effectList saves the corresponding props changes. After that, it will traverse the effectList to perform the corresponding dom operations and life cycle, hooks callback or destroy functions. What each function does is as follows
The commitRootImpl function is actually scheduled in the commitRoot function
//ReactFiberWorkLoop.old.js function commitRoot(root) { var renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority$1(ImmediatePriority$1, commitRootImpl.bind(null, root, renderPriorityLevel)); return null; }
The commitRootImpl function is mainly divided into three parts:
-
Pre work in commit phase
-
Call flushpassive effects to complete the task of all effects
-
Initialize related variables
-
Assign firstEffect to the following traversal of the effectList
//ReactFiberWorkLoop.old.js do { // Call flushpassive effects to complete the task of all effects flushPassiveEffects(); } while (rootWithPendingPassiveEffects !== null); //... // The reset variable finishedWork refers to rooFiber root.finishedWork = null; //Reset priority root.finishedLanes = NoLanes; // Scheduler callback function reset root.callbackNode = null; root.callbackId = NoLanes; // Reset global variables if (root === workInProgressRoot) { workInProgressRoot = null; workInProgress = null; workInProgressRootRenderLanes = NoLanes; } else { } //rootFiber may have new side effects and add it to effectLis let firstEffect; if (finishedWork.effectTag > PerformedWork) { if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { firstEffect = finishedWork.firstEffect; }
-
-
mutation stage
Traverse the effectList and execute three methods, commitBeforeMutationEffects, commitMutationEffects and commitLayoutEffects, respectively, to execute the corresponding dom operation and life cycle
When introducing the dual cache Fiber tree, after building the workInProgress Fiber tree, we will point the current of the fiberRoot to the workInProgress Fiber to make the workInProgress Fiber current. This step occurs after the commitMutationEffects function is executed and before commitLayoutEffects, because componentWillUnmount occurs in the commitMutationEffects function, At this time, you can also get the previous Update, and componentDidMount and componentDidUpdate will be executed in commitLayoutEffects. At this time, you can get the updated real dom
function commitRootImpl(root, renderPriorityLevel) { //... do { //... commitBeforeMutationEffects(); } while (nextEffect !== null); do { //... commitMutationEffects(root, renderPriorityLevel);//commitMutationEffects } while (nextEffect !== null); root.current = finishedWork;//Switch current Fiber tree do { //... commitLayoutEffects(root, lanes);//commitLayoutEffects } while (nextEffect !== null); //... }
-
After mutation
-
Assign relevant variables according to rootdoeshavepassive effects
-
Execute flushSyncCallbackQueue to handle life cycles such as componentDidMount or synchronization tasks such as uselayouteeffect
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; // Assign relevant variables according to rootdoeshavepassive effects if (rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = false; rootWithPendingPassiveEffects = root; pendingPassiveEffectsLanes = lanes; pendingPassiveEffectsRenderPriority = renderPriorityLevel; } else {} //... // Ensure scheduled ensureRootIsScheduled(root, now()); // ... // Execute flushSyncCallbackQueue to handle life cycles such as componentDidMount or synchronization tasks such as uselayouteeffect flushSyncCallbackQueue(); return null;
-
Now let's look at what the three functions in the mutation phase do respectively
-
commitBeforeMutationEffects
This function mainly does the following two things-
Execute getSnapshotBeforeUpdate
In the source code, the function corresponding to commitbeforemultioneffectonfiber is commitbeforemultionlifecycles, in which getSnapshotBeforeUpdate will be called. Now we know that getSnapshotBeforeUpdate is executed in the commitbeforemultioneffect function in the mutation phase, and the commit phase is synchronous, so getSnapshotBeforeUpdate is also executed synchronouslyfunction commitBeforeMutationLifeCycles( current: Fiber | null, finishedWork: Fiber, ): void { switch (finishedWork.tag) { //... case ClassComponent: { if const instance = finishedWork.stateNode; const snapshot = instance.getSnapshotBeforeUpdate(//getSnapshotBeforeUpdate finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); } }
-
Scheduling useEffect
In the flushPassiveEffects function, flushPassiveEffectsImpl is called to traverse pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount, and the corresponding effect callbacks and destruct functions are executed. These two arrays are assigned in commitLayoutEffects function (which will be mentioned later). effectList is assigned to rootWithPendingPassiveEffects after mutation. Then scheduleCallback schedules the execution of flushpassive effects
function flushPassiveEffectsImpl() { if (rootWithPendingPassiveEffects === null) {//After mutation, it becomes root return false; } const unmountEffects = pendingPassiveHookEffectsUnmount; pendingPassiveHookEffectsUnmount = [];//Callback function of useEffect for (let i = 0; i < unmountEffects.length; i += 2) { const effect = ((unmountEffects[i]: any): HookEffect); //... const destroy = effect.destroy; destroy(); } const mountEffects = pendingPassiveHookEffectsMount;//Destruction function of useEffect pendingPassiveHookEffectsMount = []; for (let i = 0; i < mountEffects.length; i += 2) { const effect = ((unmountEffects[i]: any): HookEffect); //... const create = effect.create; effect.destroy = create(); } }
componentDidUpdate or componentDidMount will be executed synchronously in the commit phase (which will be discussed later), and useEffect will be scheduled asynchronously in the commit phase, so it is applicable to the processing of side effects such as data requests
Note that, like the fiber node in the render phase, which will be labeled with Placement, useEffect or useLayoutEffect also has a corresponding effect Tag. In the source code, the corresponding export const passive = / * * / 0b0000000000 1000000000;
function commitBeforeMutationEffects() { while (nextEffect !== null) { const current = nextEffect.alternate; const effectTag = nextEffect.effectTag; // getSnapshotBeforeUpdate is executed in the commitBeforeMutationEffectOnFiber function if ((effectTag & Snapshot) !== NoEffect) { commitBeforeMutationEffectOnFiber(current, nextEffect); } // scheduleCallback scheduleuseeffect if ((effectTag & Passive) !== NoEffect) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects(); return null; }); } } nextEffect = nextEffect.nextEffect;//Traverse the effectList } }
-
-
commitMutationEffects
commitMutationEffects mainly does the following things-
Call commitDetachRef to unbind ref (explained in hook in Chapter 11)
-
Perform the corresponding dom operation according to the effectTag
-
The uselayouteeffect destroy function is executed at UpdateTag
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { //Traverse the effectList while (nextEffect !== null) { const effectTag = nextEffect.effectTag; // Call commitDetachRef to unbind ref if (effectTag & Ref) { const current = nextEffect.alternate; if (current !== null) { commitDetachRef(current); } } // Perform the corresponding dom operation according to the effectTag const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating); switch (primaryEffectTag) { // Insert dom case Placement: { commitPlacement(nextEffect); nextEffect.effectTag &= ~Placement; break; } // Insert update dom case PlacementAndUpdate: { // insert commitPlacement(nextEffect); nextEffect.effectTag &= ~Placement; // to update const current = nextEffect.alternate; commitWork(current, nextEffect); break; } //... // Update dom case Update: { const current = nextEffect.alternate; commitWork(current, nextEffect); break; } // Delete dom case Deletion: { commitDeletion(root, nextEffect, renderPriorityLevel); break; } } nextEffect = nextEffect.nextEffect; } }
Now let's look at these functions that operate on dom
commitPlacement insert node:
The simplified code is very clear. Find the nearest parent node and brother node of the node, and then judge whether to insert it before or after the parent node according to the isContainer
unction commitPlacement(finishedWork: Fiber): void { //... const parentFiber = getHostParentFiber(finishedWork);//Find the nearest parent let parent; let isContainer; const parentStateNode = parentFiber.stateNode; switch (parentFiber.tag) { case HostComponent: parent = parentStateNode; isContainer = false; break; //... } const before = getHostSibling(finishedWork);//Find sibling node if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { insertOrAppendPlacementNode(finishedWork, before, parent); } }
Commit work update node:
You can see in the simplified source code
If the tag of fiber is SimpleMemoComponent, commitHookEffectListUnmount will be called to execute the corresponding hook destruction function. You can see that the passed parameter is HookLayout | HookHasEffect, that is, execute the destruction function of uselayouteeffect.
If it is a HostComponent, call commitUpdate. commitUpdate will finally call updateDOMProperties to handle the dom operation of the corresponding Update
function commitWork(current: Fiber | null, finishedWork: Fiber): void { if (!supportsMutation) { switch (finishedWork.tag) { //... case SimpleMemoComponent: { commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork); } //... } } switch (finishedWork.tag) { //... case HostComponent: { //... commitUpdate( instance, updatePayload, type, oldProps, newProps, finishedWork, ); } return; } }
function updateDOMProperties( domElement: Element, updatePayload: Array<any>, wasCustomComponentTag: boolean, isCustomComponentTag: boolean, ): void { // TODO: Handle wasCustomComponentTag for (let i = 0; i < updatePayload.length; i += 2) { const propKey = updatePayload[i]; const propValue = updatePayload[i + 1]; if (propKey === STYLE) { setValueForStyles(domElement, propValue); } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { setInnerHTML(domElement, propValue); } else if (propKey === CHILDREN) { setTextContent(domElement, propValue); } else { setValueForProperty(domElement, propKey, propValue, isCustomComponentTag); } } }
commitDeletion delete node:
If it is a ClassComponent, it will execute componentWillUnmount and delete fiber. If it is a FunctionComponent, it will delete ref and execute the destruction function of useEffect. For details, see unmountHostComponents, commitNestedUnmounts and detachFiberMutation in the source codefunction commitDeletion( finishedRoot: FiberRoot, current: Fiber, renderPriorityLevel: ReactPriorityLevel, ): void { if (supportsMutation) { // Recursively delete all host nodes from the parent. // Detach refs and call componentWillUnmount() on the whole subtree. unmountHostComponents(finishedRoot, current, renderPriorityLevel); } else { // Detach refs and call componentWillUnmount() on the whole subtree. commitNestedUnmounts(finishedRoot, current, renderPriorityLevel); } const alternate = current.alternate; detachFiberMutation(current); if (alternate !== null) { detachFiberMutation(alternate); } }
-
-
commitLayoutEffects
After commitMutationEffects, all dom operations have been completed and the dom can be accessed. commitLayoutEffects mainly does-
Call commitLayoutEffectOnFiber to execute related life cycle functions or hook related callback
-
Execute committatchref to assign a value to ref
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; // Call commitLayoutEffectOnFiber to execute lifecycle and hook if (effectTag & (Update | Callback)) { const current = nextEffect.alternate; commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); } // ref assignment if (effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; } }
commitLayoutEffectOnFiber:
In the source code, the alias of the commitLayoutEffectOnFiber function is commitLifeCycles. In the simplified code, we can see that commitLifeCycles will determine the type of fiber, SimpleMemoComponent will perform callback of useLayoutEffect, then schedule useEffect, ClassComponent will execute componentDidMount or componentDidUpdate, and this.setState second parameters will also be executed. HostRoot executes the third parameter of the ReactDOM.render function, for example
ReactDOM.render(<App />, document.querySelector("#root"), function() { console.log("root mount"); });
Now you can know that useLayoutEffect is executed synchronously in the commit phase, and useEffect will be scheduled asynchronously in the commit phase
function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedLanes: Lanes, ): void { switch (finishedWork.tag) { case SimpleMemoComponent: { // This function calls the callback of useLayoutEffect commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // push effect into pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount // And schedule them schedulePassiveEffects(finishedWork); } case ClassComponent: { //Conditional judgment instance.componentDidMount(); //Conditional judgment instance.componentDidUpdate(//update is executed synchronously during layout prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); } case HostRoot: { commitUpdateQueue(finishedWork, updateQueue, instance);//render third parameter } } }
In schedulepassive effects, the destruction and callback functions of useEffect will be push ed into pendingpassive hookeffects unmount and pendingpassive hookeffects mount
function schedulePassiveEffects(finishedWork: Fiber) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { const {next, tag} = effect; if ( (tag & HookPassive) !== NoHookEffect && (tag & HookHasEffect) !== NoHookEffect ) { //Push the destruction function of useeffect and add scheduling enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); //Push the callback function of useeffect and add it to the schedule enqueuePendingPassiveHookEffectMount(finishedWork, effect); } effect = next; } while (effect !== firstEffect); } }
commitAttachRef:
Committattacref determines the type of ref, executes ref or assigns a value to ref.current
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance); break; default: instanceToUse = instance; } if (typeof ref === "function") { // Execute ref callback ref(instanceToUse); } else { // If it is the type of value, it is assigned to ref.current ref.current = instanceToUse; } } }
-