React source code analysis series – render phase of react (III): completeunitofwork

Time:2022-5-10

Series article directory (synchronous update)

This series is all about react V17 0.0-alpha source code

performUnitOfWork

memoryReact source code analysis series – render phase of react (I): introduction to basic processIntroduced inperformUnitOfWorkmethod:

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork. alternate; //  The corresponding fiber node on the current tree may be null
  // ... ellipsis

  let next; //  Used to store the results returned by beginwork()
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  // ... ellipsis
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  If (next = = = null) {// beginwork returns null, indicating that there is no (or no need to pay attention to) the child fiber node of the current node
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; //  The main body of the while loop of the next workloopsync / workloopconcurrent is the child fiber node
  }

  // ... ellipsis
}

As the “return” phase of render, it can only be executed after the “delivery” phase of render is completed; In other words, when beginwork returns a null value, that isThe current node does not have (or need not pay attention to) the child fiber node of the current nodeWill enter the “return” stage of render——completeUnitOfWork

completeUnitOfWork

Let’s look at the protagonist of this article——completeUnitOfWorkmethod:

function completeUnitOfWork(unitOfWork: Fiber): void {
  /*
    Complete some processing of the current fiber node
    After processing, if the current node still has a sibling node, end the current method and enter the next performnunitofwork loop
    If there is no sibling node, the parent node (completedwork. Return) will be returned,
    Until the parent node is null, it indicates that the whole workinprogress fiber tree has been processed.
   */
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    if ((completedWork.effectTag & Incomplete) === NoEffect) {
      let next;
      // ... ellipsis
      next = completeWork(current, completedWork, subtreeRenderLanes);
      // ... ellipsis
      
      /*
        If the return of completework is not empty, it will enter the next performnunitofwork loop
        However, this situation is too rare. At present, I only see that there will be a return related to suspend, so I think this code segment will not be executed for the time being
       */
      if (next !== null) {
        workInProgress = next;
        return;
      }

      // ... ellipsis

      if (
        returnFiber !== null &&
        (returnFiber.effectTag & Incomplete) === NoEffect
      ) {
        /*Collect all child fiber nodes with effecttag and mount them on the current node in the form of a linked list (effectlist)*/
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        /*If the current fiber node (completedwork) also has an effecttag, put it after the child fiber node (in the effectlist)*/
        const effectTag = completedWork.effectTag;
        /*Skip the nodes of nowork and performedwork effecttag. There is no need to explain nowork. Performedwork is for devtools*/
        if (effectTag > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      //Exception handling, omitting
    }

    //Take the sibling node of the current fiber node (completedwork);
    //If there is a value, the completeunitofwork is ended, and the sibling node is used as the main body (unitofwork) of the next performnunitofwork
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    //If there is no sibling node, the next do Process the parent node in the while loop (completedwork. Return)
    completedWork = returnFiber;
    //Note here!
    //Although workinprogress is set to completedwork, it is meaningless because there is no return, that is, it does not end completeunitofwork
    //Until completedwork (actually completedwork. Return in this loop) is null, end do After the while loop
    //At this time, the running result (workinprogress) of completeunitofwork is null
    //It also means that the while loop in performsyncworkonroot / performcurrentworkonroot has reached the end condition
    workInProgress = completedWork;
  } while (completedWork !== null);

  //Omit
}

Please see the flow chart:

React source code analysis series - render phase of react (III): completeunitofwork

According to the flow chart,completeUnitOfWorkTwo main things have been done: implementationcompleteWorkandCollapse effectlist, let’s introduce these two parts in detail.

completeWork

If the beginwork method in the “delivery” stage is mainly to create child nodes, then the beginwork method in the “return” stagecompleteWorkThe main method is to create the DOM node of the current node and collapse the DOM node and effectlist of the child node.
Similar to beginwork,completeWorkThe current node will also be differenttagTypes perform different logic:

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ... ellipsis
      return null;
    }
    case HostRoot: {
      // ... ellipsis
      return null;
    }
    case HostComponent: {
      // ... ellipsis
      return null;
    }
  // ... ellipsis
}

It should be noted that many types of nodes do not have the logic of completework (that is, they are directly connected without doing anything)return null)For example, very commonFragmentandFunctionComponent。 We focus on what is necessary for page renderingHostComponent, i.eHTML tags(e.g<div></div>)Fiber node converted to.

Process hostcomponent

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    // ... ellipsis
    case HostComponent: {
      // ... ellipsis
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

        // ... ellipsis
      } else {
        // ... ellipsis
        const instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );

        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance;

        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
      }
      return null;
    }
    // ... ellipsis
  }
}

From the above code segment, we can know that the processing of hostcomponent by completework method mainly has two code branches:

  • (current !== null && workInProgress.stateNode != null) === trueThe current node is modified“to update”Operation;
  • (current !== null && workInProgress.stateNode != null) === trueThe current node is modified“newly build”Operation;

The reason why mount (first screen rendering) and update commonly used in previous articles are not used here is that there is a situation that iscurrent !== nullandworkInProgress.stateNode === null: during update, if the current fiber node is a new node and has been marked with placement effecttag in the beginwork phase, then the statenode will be null; In this case, the same needs to be done“newly build”Operation.

React source code analysis series - render phase of react (III): completeunitofwork

Update operation for hostcomponent

In this code branch, because it has been determinedworkInProgress.stateNode !== null, that is, the corresponding DOM node already exists, so there is no need to generate DOM nodes.

We can see that this is mainly the implementation of oneupdateHostComponentmethod:

updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container,
) {
  /*If props does not change (the current node is reused through the bailoutonalreadyfinished work method), you can skip the processing of the current node*/
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    return;
  }

  const instance: Instance = workInProgress.stateNode;
  //Omit
  /*Calculate the DOM node attributes that need to be changed and store them in an array (the elements of the even index of the array are the attribute names, and the elements of the cardinality index of the array are the attribute values)*/
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext,
  );
  //Mount the calculated updatepapayload in workinprogress On updatequeue for subsequent commit phases
  workInProgress.updateQueue = (updatePayload: any); 
  //If updatepayload is not empty, update's effecttag will be marked on the current node
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};

As you can see from the code snippet aboveupdateHostComponentThe main function of is to calculate the DOM node attributes that need to be changed and mark the current node with the update effecttag.

prepareUpdate

Next let’s seeprepareUpdateHow to calculate the DOM node attributes that need to be changed:

export function prepareUpdate(
  domElement: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): null | Array<mixed> {
  //Omit dev code
  return diffProperties(
    domElement,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
  );
}

It can be seen thatprepareUpdateIn fact, the diffproperties method is called directly.

diffProperties

diffPropertiesThere are a lot of methods. I won’t put the source code here. Let’s talk about the process:

  1. Do special processing for lastprops & nextprops of a specific tag (because this scenario deals with hostcomponent, so tag is the HTML tag name), including input / select / textarea. For example, the value value of input may be a number, while the value of native input only accepts string, so the data type needs to be converted.
  2. Traverse lastprops:

    1. If the prop also exists in nextprops, skip it, which means that the prop has not changed and does not need to be processed.
    2. When you see the prop with style, it is sorted into the styleupdates variable (object), and this part of the style attribute is set to null value
    3. Push the propkey other than the above into an array (updatepapayload), and then push a null value into the array, which means that the prop is cleared.
  3. Traverse nextprops:

    1. If the nextprop is consistent with lastprop, that is, there is no change before and after the update, skip.
    2. When you see prop with style, sort it into the styleupdates variable. Note that this part of the style attribute has value
    3. Handling dangerously_ SET_ INNER_ HTML
    4. Dealing with children
    5. In addition to the above scenarios, the key and value of prop are directly pushed into the array (updatepapayload).
  4. If styleupdates is not empty, push the ‘style’ and styleupdates variables into the array (updatepapayload).
  5. Returns updatepapayload.

updatePayloadIs an array in which the elements of the even index of the array areprop key, the element of array cardinality index isprop value

markUpdate

Let’s go onmarkUpdateMethod, the method is actually very simple, that is, inworkInProgress.effectTagHit aUpdate EffectTag

function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  workInProgress.effectTag |= Update;
}

New operation for hostcomponent

The main logic of “new” operation includes three aspects:

  • Generate the corresponding DOM node for the fiber node: createinstance method
  • Insert the descendant DOM node into the newly generated DOM node: appendallchildren method
  • Initialize all properties of the current DOM node and event callback processing: finalizeinitialchildren method
createInstance

Let’s see“Generate the corresponding DOM node for the fiber node”Method of——createInstance

export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  //Omit dev snippet
  //Determine the namespace (xmlns attribute) of the DOM node, generally“ http://www.w3.org/1999/xhtml "
  parentNamespace = ((hostContext: any): HostContextProd); 
  //Create DOM element
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  //Create an attribute (pointer) pointing to the fiber node object on the DOM object to facilitate subsequent access
  precacheFiberNode(internalInstanceHandle, domElement);
  //Create an attribute (pointer) pointing to props on the DOM object to facilitate subsequent access
  updateFiberProps(domElement, props);
  return domElement;
}

It can be seen that createinstance mainly calls the createElement method to create DOM elements; As for createElement, this article will not expand. If you are interested, you can have a lookSource code

appendAllChildren

Let’s look at the method of “inserting the descendant DOM node into the newly generated DOM node”——appendAllChildren :

//Completework is called as follows: appendallchildren (instance, workinprogress, false, false);

appendAllChildren = function(
  Parent: instance, // relative to the child node to append, the node currently processed by completework is the parent node
  workInProgress: Fiber,
  needsVisibilityToggle: boolean,
  isHidden: boolean,
) {
  let node = workInProgress. child; //  First sub fiber node
  /*This while loop is essentially a depth first traversal*/
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
      //If it is a child node corresponding to HTML tag or plain text, add the dom of the current child node to the end of the DOM child node list of the parent node
      appendInitialChild(parent, node.stateNode);
    }Else if (enablefundamentalapi & & node. Tag = = = fundamentalcomponent) {// ignore first
      appendInitialChild(parent, node.stateNode.instance);
    } else if (node.tag === HostPortal) {
      // ... No operation
    } else if (node.child !== null) {
      //For some special types of child nodes, such as < fragment / >, try to get DOM from the child nodes of the child nodes
      node. child. return = node; //  Set the return pointer to facilitate subsequent identification of whether the loop end condition is met
      node = node. child; //  The loop body changes from a child node to a child node of a child node
      continue; //  Start a new cycle immediately
    }
    if (node === workInProgress) {
      return; //  When traversing "regression", it is found that the end condition of traversal has been reached, and the traversal is ended
    }
    //If the current circular principal node has no siblings, perform "regression"; And if it is found that there is still no sibling after the "regression", the "regression" will continue
    while (node.sibling === null) {
      if (node.return === null || node.return === workInProgress) {
        return; //  When the end condition of traversal is reached in the process of "regression", the traversal is ended
      }
      node = node. return; //  Result of "regression": add node Return is the subject of the next cycle
    }
    //This indicates that the current loop body has sibling
    node. sibling. return = node. return; //  Set the return pointer to facilitate subsequent identification of whether the loop end condition is met
    node = node. sibling; //  Add node Sibling is the body of the next cycle
  }
};

//Appendinitialchild essentially executes the native DOM node method appendChild
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void {
  parentInstance.appendChild(child);
}

appendAllChildrenIt is essentially a depth first traversal with conditional constraints (limiting progressive levels):

  1. Take the first child node of the current node (parent) as the loop body(node)。
  2. If the loop body isFiber node corresponding to HTML tag or plain text, the DOMappendChildtoparent
  3. If the current loop body(node)Have sibling nodes(node.sibling), set the sibling node as the main body of the next cycle.

Just looking at the above process, isn’t it a typical breadth first traversal? Don’t worry, because there is another special case: when the current cycle subject is notFiber node corresponding to HTML tag or plain text, and the current cycle body has child nodes(node.child)When, the child node of the current cycle body is taken as the body of the next cycle, and the next cycle starts immediately(continue)。

Take the following component as an example:

function App() {
    return (
        <div>
            <b>1</b>
            <Fragment>
                <span>2</span>
                <p>3</p>
            </Fragment>
        </div>
    )
}

according toReact source code analysis series – render phase of react (I): introduction to basic processThe execution sequence of beginwork and completework in can be obtained:

1. rootFiber beginWork 
2. App Fiber beginWork 
3. div Fiber beginWork 
4. b Fiber beginWork 
5. B Fiber completework // current node - < B / >, appendChild text node
6. Fragment Fiber beginWork
7. span Fiber beginWork
8. Span fiber completework // current node - < span / >, appendChild text node
9. p Fiber beginWork
10. P fiber completework // current node - < P / >, appendChild text node
11. Fragment fiber completework // skip
12. Div fiber completework // let's focus on this one
13. App Fiber completeWork
14. rootFiber completeWork

Let’s focus on the div nodeappendAllChildren

  1. Initialization before executing the while loop: take out the first child node of div node – node B as the body of the first while loop.
  2. The first while loop (the main body of the loop is node b):

    1. Node B is a hostcomponent, which directly appendChild.
    2. B node has a sibling node, that is, the fragment node, which is set as the body of the next while loop(node)。
  3. The second while loop (the main body of the loop is the fragment node):

    1. Since the fragment node is neither hostcomponent nor hosttext, the span node, the first child node of the fragment node, will be taken as the body of the next while loop(node)。
    2. Enter immediately(continue)The next while loop.
  4. The third while loop (the main body of the loop is the span node):

    1. The span node is a hostcomponent and directly appendChild.
    2. Span node has a sibling node, P node, which is set as the body of the next while loop(node)。
  5. The fourth while loop (the main body of the loop is the P node):

    1. The P node is a hostcomponent and directly appendChild.
    2. The P node has no sibling nodes, so regression is performed(node = node.return)At this point, in this “regression” snippet — ASmall while loopIn, the loop body becomes the parent node of the P node, that is, the fragment node.
    3. Continue next timeSmall while loop: because the fragment has no sibling nodes, it is not satisfiedSmall while loopTherefore, continue the “regression”, and at this time, the main body of the cycle(node)Is a div node.
    4. Continue next timeSmall while loop: because div node meetsnode.return === workInProgressTherefore, directly end the whole traversal process – appendallchildren.
finalizeInitialChildren

Let’s look at “initializing all properties of the current DOM node and event callback processing”——finalizeInitialChildren

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

From the above code segment, we can clearly see that finalizeinitialchildren is mainly divided into two steps:

  1. implementsetInitialPropertiesmethod; Note that this method is different from prepareupdate. This method will actually mount the DOM attribute to the DOM node and call itaddEventListenerBind the event processing callback to the current DOM node.
  2. implementshouldAutoFocusHostComponentMethod: Returnprops.autoFocusValue of (only)button / input / select / textareaSupport).

Collapse effectlist

As the basis of DOM operation, the commit phase needs to find all fiber nodes with effecttag and perform the corresponding operations of effecttag in turn. Do you still need to traverse the fiber tree again in the commit phase? This is obviously inefficient.

In order to solve this problem, in completeunitofwork, each fiber node that executes completework and has an effecttag will be saved in a one-way linked list called effectlist; The first fiber node in the effectlist is saved in fiber Firsteffect, the last element is saved in fiber lastEffect 。

The parent node of “allegist” will be added in the “allegist. Child”, which is similar to the parent node of “allegist” One way linked list with firsteffect as the starting point.

If the current fiber node (completedwork) also has an effecttag, put it after the child fiber node (in the effectlist).

/*If the head pointer of the effectlist of the parent node is null, then directly assign the head pointer of the effectlist of this node to the head pointer of the parent node, which is equivalent to hanging the entire effectlist of this node directly in the parent node*/
if (returnFiber.firstEffect === null) {
    returnFiber.firstEffect = completedWork.firstEffect;
}
/*If the effectlist of the parent node is not empty, mount the effectlist of this node after the effectlist of the parent node*/
if (completedWork.lastEffect !== null) {
    if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
    }
    returnFiber.lastEffect = completedWork.lastEffect;
}

/*If the current fiber node (completedwork) also has an effecttag, put it after the child fiber node (in the effectlist)*/
const effectTag = completedWork.effectTag;
/*Skip the nodes of nowork and performedwork effecttag. There is no need to explain nowork. Performedwork is for devtools*/
if (effectTag > PerformedWork) {
  if (returnFiber.lastEffect !== null) {
     returnFiber.lastEffect.nextEffect = completedWork;
  } else {
     returnFiber.firstEffect = completedWork;
  }
     returnFiber.lastEffect = completedWork;
  }
}

Completeunitofwork end

Completeunitofwork has two end scenarios:

  • Current node(completed)Have sibling nodes(completed.sibling)At this time, workinprogress (i.e. the loop body of performinutofwork) will be set as the brother node, and then the completeunitofwork method will be ended. After that, the next performinutofwork will be carried out, in other words, the “delivery” phase of the “brother node” – beginwork will be executed.
  • During the “return” of completeunitofwork,completedThe value of isnull, that is, the regression of the whole fiber tree has been completed; At this time, the value of workinprogress is null, which means that the while loop in workloopsync / workloopconcurrent method has also reached the end condition; So far, react’srenderEnd of phase.

When the render phase ends, theperformSyncWorkOnRootMethod is calledcommitRoot(root)(here)rootParameter passing refers to fiberrootnode) to start reactcommitWork in this stage.

Recommended Today

P1851 Good friend (Todo)

good friend topic background Xiao Keke and all the other students have a RFID serial number plate on their wrist so that the teacher can easily count their number. Many students have a &quot;good friend&quot;. if\(A\)The sum of the divisors of the serial numbers is exactly equal to\(B\)serial number, then\(A\)'s good friend is\(B\). Here, the […]