This is the 10th day of my participation in the August More Text Challenge. For details, see:August is more challenging

Before formally analyzing the Rendering of the Fiber tree, let’s once again review the four stages of the reconciler’s operational process:

  1. Input stage: connectionreact-domPackage,Fiber updateRequest (refer toThe React app startup process).
  2. Registering scheduling tasks: With the scheduling center (schedulerPackage) to register for scheduling taskstask, waiting for a task callback (seeReact Scheduler Scheduler).
  3. Execution task callback: constructed in memoryFiber treeandDOMObject (reference)Fiber tree construct (first created)And the Fiber tree (contrast update)).
  4. Output: With the renderer (react-dom) interaction, renderingDOMNode.

In the fourth stage (output) analyzed in this section, the Fiber tree rendering is important because it is the last step in the pipeline of reconciler operations, or because all previous steps serve the last step.

Now that I’ve introduced the fiber tree structure, I’m going to analyze the fiber tree rendering process, which is actually a further processing of the fiber tree.

Fiber tree characteristics

Through the above interpretation of fiber tree structure, the basic characteristics of fiber tree can be summarized:

  • Whether it isFor the first time to constructOr is itCompared to update, which will eventually generate a tree in memory for rendering the pageFiber tree(i.e.fiberRoot.finishedWork).
  • This one is going to be renderedFiber treeThere are two characteristics:
    1. The side effect queue is mounted on the root node (specificallyfinishedWork.firstEffect)
    2. Representing the latest pageDOMThe object is mounted onFiber treeIn the firstHostComponentType of node (specificallyDOMObject is mounted onfiber.stateNodeAttributes)

Once again, the two fiber trees used in the previous article can verify the above characteristics:

  1. Primary structural

  1. Compared to update

commitRoot

The entire rendering logic is in the commitRoot function:

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}
Copy the code

Both render and scheduling priorities are used in commitRoot. The discussion of priorities has already been explained earlier (see Priority management in React and Fiber tree construction (foundation preparation)# priorities) and will not be discussed in this section. The final implementation is through the commitRootImpl function:

/ /... Omit some irrelevant code
function commitRootImpl(root, renderPriorityLevel) {
  // ============ Before rendering: prepare ============

  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;

  // Clear the FiberRoot object properties
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  root.callbackNode = null;

  if (root === workInProgressRoot) {
    // Reset the global variable
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  }

  // Update the side effect queue again
  let firstEffect;
  if (finishedWork.flags > PerformedWork) {
    // By default, the fiber node's side effect queue does not include itself
    // If the root node has side effects, add the root node to the end of the side effects queue
    if(finishedWork.lastEffect ! = =null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else{ firstEffect = finishedWork; }}else {
    firstEffect = finishedWork.firstEffect;
  }

  // ============ render ============
  let firstEffect = finishedWork.firstEffect;
  if(firstEffect ! = =null) {
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;
    // Stage 1: before the DOM mutation
    nextEffect = firstEffect;
    do {
      commitBeforeMutationEffects();
    } while(nextEffect ! = =null);

    // Stage 2: DOM mutation, interface change
    nextEffect = firstEffect;
    do {
      commitMutationEffects(root, renderPriorityLevel);
    } while(nextEffect ! = =null);
    // Restore the interface status
    resetAfterCommit(root.containerInfo);
    // Switch the current pointer
    root.current = finishedWork;

    // Phase 3: Layout phase, calling life cycle componentDidUpdate and callback functions
    nextEffect = firstEffect;
    do {
      commitLayoutEffects(root, lanes);
    } while(nextEffect ! = =null);
    nextEffect = null;
    executionContext = prevExecutionContext;
  }

  // ============ after rendering: reset and clean ============
  if (rootDoesHavePassiveEffects) {
    // Have passive action (useEffect), save some global variables
  } else {
    // Decompose the list of side effect queues to assist garbage collection
    // If there is a passive action (useEffect), put the decomposition in the flushPassiveEffects function
    nextEffect = firstEffect;
    while(nextEffect ! = =null) {
      const nextNextEffect = nextEffect.nextEffect;
      nextEffect.nextEffect = null;
      if(nextEffect.flags & Deletion) { detachFiberAfterEffects(nextEffect); } nextEffect = nextNextEffect; }}// Reset some global variables (omitted)...
  // The following code is used to check if there is a new update task
  // For example, in componentDidMount, call setState() again

  // 1. Detect routine (asynchronous) tasks, and initiate asynchronous scheduling (scheduler 'can only be called asynchronously)
  ensureRootIsScheduled(root, now());
  // 2. Check for a synchronization task and call flushSyncCallbackQueue(no need to wait for scheduler to schedule again) to enter the fiber tree construction loop again
  flushSyncCallbackQueue();

  return null;
}
Copy the code

The commitRootImpl function divides the entire commitRootImpl into three segments (before, render, and after), depending on whether rendering is called.

Before rendering

Do some preparatory work for the next formal rendering. Mainly include:

  1. Set global status (for example, update)fiberRootProperties on
  2. Reset global variables (e.g.workInProgressRoot.workInProgressEtc.)
  3. Update the side effect queue again: for the root node onlyfiberRoot.finishedWork
    • By default, the root node’s side effect queue does not include itself. If the root node has side effects, the root node is added to the end of the side effect queue
    • Notice it just lengthens the side effect queue, butfiberRoot.lastEffectThe pointer doesn’t change. For example, when first constructed, the root node hasSnapshotTags:

Apply colours to a drawing

The main logic in the rendering phase of the commitRootImpl function is to process the side effect queue and render the latest DOM node (already in memory but not yet rendered) to the interface.

The whole rendering process is distributed into 3 functions:

  1. commitBeforeMutationEffects
    • Dom changes before processing side effects queue withSnapshot.PassiveOf the tagfiberNode.
  2. commitMutationEffects
    • The DOM changes and the interface is updated. Processing side effects queue withPlacement.Update.Deletion.HydratingOf the tagfiberNode.
  3. commitLayoutEffects
    • After dom changes, handle side effects withUpdate | CallbackOf the tagfiberNode.

From the above source code analysis, commitRootImpl’s responsibilities can be summarized into two areas:

  1. Process the side effect queue. (Step 1, step 2 and Step 3 are both processed, only the node id is processedfiber.flagsDifferent).
  2. Call the renderer and print the final result (in Step 2:commitMutationEffectsPerformed).

CommitRootImpl processes fiberRoot. FinishedWork the fiber tree that is about to be rendered. In theory, it is not necessary to care how the fiber tree is generated (either by first construction or by contrast update). For clarity and simplicity, all the diagrams below use the fiber tree structure created the first time.

The objects that these three functions deal with are side effect queues and DOM objects.

No matter how complex the fiber tree structure is, at commitRoot stage, only two nodes actually work:

  • Side effect queueNode: The root node, that isHostRootFiberNode.
  • DOM objectNode: First node from top to bottomHostComponentThe type offiberNode, this nodefiber.stateNodeIt actually points to the latest DOM tree.

For clarity, we omit some of the irrelevant references, leaving only the fiber nodes that are actually used in the commitRoot phase:

commitBeforeMutationEffects

Phase 1: Process the Fiber node with Snapshot,Passive tags in the side effect queue before dom changes.

/ /... Omit some irrelevant code
function commitBeforeMutationEffects() {
  while(nextEffect ! = =null) {
    const current = nextEffect.alternate;
    const flags = nextEffect.flags;
    // Process the 'Snapshot' tag
    if((flags & Snapshot) ! == NoFlags) { commitBeforeMutationEffectOnFiber(current, nextEffect); }// Process the 'Passive' flag
    if((flags & Passive) ! == NoFlags) {// The Passive flag only appears when hook is used. So here is the processing of the hook object
      if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
        scheduleCallback(NormalSchedulerPriority, () = > {
          flushPassiveEffects();
          return null; }); } } nextEffect = nextEffect.nextEffect; }}Copy the code
  1. To deal withSnapshottag
function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
) :void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      return;
    }
    case ClassComponent: {
      if (finishedWork.flags & Snapshot) {
        if(current ! = =null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;

          constsnapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState, ); instance.__reactInternalSnapshotBeforeUpdate = snapshot; }}return;
    }
    case HostRoot: {
      if (supportsMutation) {
        if (finishedWork.flags & Snapshot) {
          constroot = finishedWork.stateNode; clearContainer(root.containerInfo); }}return;
    }
    case HostComponent:
    case HostText:
    case HostPortal:
    case IncompleteClassComponent:
      return; }}Copy the code

As you can see from the source code, the only types associated with the Snapshot tag are ClassComponent and HostRoot.

  • forClassComponentType node, calledinstance.getSnapshotBeforeUpdateLife cycle function
  • forHostRootType node, callclearContainerEmpty the container node (i.ediv#rootThe DOM node).
  1. To deal withPassivetag

The Passive flag exists only on function nodes that use hook objects. For details about the subsequent operations, see hook principles. Here we need to know that in the first phase of commitRoot, in order to process hook objects (such as useEffect), a scheduler task is registered separately through scheduleCallback, waiting for the scheduler to process.

Note: All tasks scheduled through the Scheduler are triggered by MessageChannel and are asynchronously executed (see React Scheduler).

Test:

// In the following example code, the output order is 1, 3, 4, 2
function Test() {
  console.log(1);
  useEffect(() = > {
    console.log(2);
  });
  console.log(3);
  Promise.resolve(() = > {
    console.log(4);
  });
  return <div>test</div>;
}
Copy the code

commitMutationEffects

Phase 2: THE DOM changes and the interface is updated. Processes fiber nodes in the side effect queue with ContentReset, Ref, Placement, Update, Deletion, Hydrating tags.

/ /... Omit some irrelevant code
function commitMutationEffects(root: FiberRoot, renderPriorityLevel: ReactPriorityLevel,) {
  / / deal with Ref
  if (flags & Ref) {
    const current = nextEffect.alternate;
    if(current ! = =null) {
      // Clear the ref (commitRoot) and reassign the value after the dom changecommitDetachRef(current); }}// Handle DOM mutations
  while(nextEffect ! = =null) {
    const flags = nextEffect.flags;
    const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    switch (primaryFlags) {
      case Placement: {
        // Add a new node
        commitPlacement(nextEffect);
        nextEffect.flags &= ~Placement; // Note that the Placement marker will be removed
        break;
      }
      case PlacementAndUpdate: {
        // Placement
        commitPlacement(nextEffect);
        nextEffect.flags &= ~Placement;
        // Update
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      case Update: {
        // Update the node
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      case Deletion: {
        // Delete the node
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break; } } nextEffect = nextEffect.nextEffect; }}Copy the code

Dealing with DOM mutations:

  1. new: Function call stackcommitPlacement -> insertOrAppendPlacementNode -> appendChild
  2. update: Function call stackcommitWork -> commitUpdate
  3. delete: Function call stackcommitDeletion -> removeChild

AppendChild, commitUpdate, removeChild functions in the React-DOM package are called. They are standard functions specified in the HostConfig protocol (source code in ReactDomHostconfig.js) and implemented in the React-dom renderer package. These functions operate directly on the DOM, so the interface is updated after execution.

Note: After commitMutationEffects executes, switch the current Fiber tree (root.current = finishedWork) in the commitRootImpl function, ensuring that fiberroot.current points to the fiber tree representing the current interface.

commitLayoutEffects

Phase 3: After dom changes, process the fiber node in the side effect queue with Update, Callback, and Ref tags.

/ /... Omit some irrelevant code
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  while(nextEffect ! = =null) {
    const flags = nextEffect.flags;
    // Handle the Update and Callback tags
    if (flags & (Update | Callback)) {
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }
    if (flags & Ref) {
      // Reset the refcommitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code

Core logic are commitLayoutEffectOnFiber – > commitLifeCycles function.

/ /... Omit some irrelevant code
function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
) :void {
  switch (finishedWork.tag) {
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.flags & Update) {
        if (current === null) {
          // First render: call componentDidMount
          instance.componentDidMount();
        } else {
          const prevProps =
            finishedWork.elementType === finishedWork.type
              ? current.memoizedProps
              : resolveDefaultProps(finishedWork.type, current.memoizedProps);
          const prevState = current.memoizedState;
          // Update phase: call componentDidUpdateinstance.componentDidUpdate( prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate, ); }}const updateQueue: UpdateQueue<
        *,
      > | null = (finishedWork.updateQueue: any);
      if(updateQueue ! = =null) {
        This.setstate ({}, callback)
        commitUpdateQueue(finishedWork, updateQueue, instance);
      }
      return;
    }
    case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      if (current === null && finishedWork.flags & Update) {
        const type = finishedWork.type;
        const props = finishedWork.memoizedProps;
        // Set the native state such as focus
        commitMount(instance, type, props, finishedWork);
      }
      return; }}}Copy the code

In the commitLifeCycles function:

  • forClassComponentNode, calling the lifecycle functioncomponentDidMountorcomponentDidUpdate, the callupdate.callbackCallback function.
  • forHostComponentNode, if anyUpdateFlag, some native states need to be set (e.g.focusEtc.)

After the rendering

After performing the above steps, the rendering task is complete. After rendering is complete, some resets and cleanups are needed:

  1. Clear the side effect queue

    • Because the side effect queue is a linked list, because the singlefiberObject reference, cannot beThe gc recycling.
    • Take the list apart, whenfiberWhen an object is no longer in use, it can beThe gc recycling.

2. Check for updates

  • Throughout the rendering process, it is possible to generate newupdate(as incomponentDidMountFunction, called againsetState()).
  • If it is a regular (asynchronous) task, no special treatment, callensureRootIsScheduledEnsure that the task has been registered with the scheduling center.
  • If it is a synchronous task, it is invoked activelyflushSyncCallbackQueue(without waiting again for scheduler scheduling), enter the fiber tree construction loop again
// Clear the side effect queue
if (rootDoesHavePassiveEffects) {
  // Have passive action (useEffect), save some global variables
} else {
  // Decompose the list of side effect queues to assist garbage collection.
  // If there is a passive action (useEffect), put the decomposition in the flushPassiveEffects function
  nextEffect = firstEffect;
  while(nextEffect ! = =null) {
    const nextNextEffect = nextEffect.nextEffect;
    nextEffect.nextEffect = null;
    if(nextEffect.flags & Deletion) { detachFiberAfterEffects(nextEffect); } nextEffect = nextNextEffect; }}// Reset some global variables (omitted)...
// The following code is used to check if there is a new update task
// For example, in componentDidMount, call setState() again

// 1. Detect routine (asynchronous) tasks, and initiate asynchronous scheduling (scheduler 'can only be called asynchronously)
ensureRootIsScheduled(root, now());
// 2. Check for a synchronization task and call flushSyncCallbackQueue(no need to wait for scheduler to schedule again) to enter the fiber tree construction loop again
flushSyncCallbackQueue();
Copy the code

conclusion

This section analyzes the processing process of Fiber tree rendering. From a macro perspective, fiber tree rendering is located in the output stage of reconciler operation process, and is the last link (from input to output) in the entire reconciler operation process. In this section, the commitRootImpl function is decomposed from three aspects: before rendering, after rendering, according to the source code. The core rendering logic is divided into three functions, which together handle the fiber node with side effects and render the latest DOM objects to the interface through the react-DOM renderer.

Write in the last

This article belongs to the diagram react source code series in the operation of the core plate, this series of nearly 20 articles, really in order to understand the React source code, and then improve the architecture and coding ability.

The first draft of the graphic section has been completed and will be updated in August. If there are any errors in the article, we will correct them as soon as possible on Github.