react source code analysis 10 Commit phase

Posted by tate on Wed, 15 Dec 2021 12:19:05 +0100

react source code analysis 10 Commit phase

Video Explanation (efficient learning): Enter learning

Previous articles:

1. Introduction and interview questions

2. Design concept of react

3.react source code architecture

4. Source directory structure and debugging

5. JSX & Core api

6.legacy and concurrent mode entry functions

7.Fiber architecture

8.render stage

9.diff algorithm

10.commit phase

11. Life cycle

12. Status update process

13.hooks source code

14. Handwritten hooks

15.scheduler&Lane

16.concurrent mode

17.context

18 event system

19. Handwritten Mini react

20. Summary & answers to interview questions in Chapter 1

21.demo

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

    1. Call flushpassive effects to complete the task of all effects

    2. Initialize related variables

    3. 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

    1. Assign relevant variables according to rootdoeshavepassive effects

    2. 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

    1. 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 synchronously

      function 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,
                );
              }
      }
      
    2. Scheduling useEffect

      In the flushPassiveEffects function, flushPassiveEffectsImpl is called to traverse pendingPassiveHookEffectsUnmount and pendingPassiveHookEffectsMount, and the corresponding effect callbacks and destruct functions are executed. The two arrays are assigned in the commitLayoutEffects function (which will be discussed later). After the mutation, the effectList is assigned to rootwithpendingpassive effects, and then the 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

    1. Call commitDetachRef to unbind ref (explained in hook in Chapter 11)

    2. Perform the corresponding dom operation according to the effectTag

    3. 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 code

      function 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

    1. Call commitLayoutEffectOnFiber to execute related life cycle functions or hook related callback

    2. 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, this.. The second parameter of setstate will also be executed, and HostRoot will execute reactdom The third parameter of the 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;
          }
        }
      }
      

Topics: Javascript Front-end React