Click on the React source debug repository.

This is the second article to look in detail at the React DOM operation, which happens during the COMMIT phase.

Inserting a DOM node operates on the stateNode on the Fiber node. For fiber nodes of the native DOM type, stateNode stores the DOM node. The commit phase inserts the DOM node into the real DOM tree by following the Fiber tree.

CommitPlacement is the entry point to insert a node,

function commitMutationEffectsImpl(fiber: Fiber, root: FiberRoot, renderPriorityLevel,) {...switch (primaryEffectTag) {
    case Placement: {
      // Insert operations
      commitPlacement(fiber);
      fiber.effectTag &= ~Placement;
      break; }... }}Copy the code

We call the fiber node that needs to be inserted the target node, and the commitPlacement function does the following:

  1. Find the parent of the DOM level of the target node
  2. Find the corresponding parent based on the destination node type
  3. If the DOM node corresponding to the target node currently has only literal content, similarly<div>hello</div>And hold the ContentReset effectTag, so set the text content before inserting the node
  4. Find the base node
  5. An insert
function commitPlacement(finishedWork: Fiber) :void {...// Find the parent of the DOM layer of the target node.
  const parentFiber = getHostParentFiber(finishedWork);

  // Change parent according to the destination node type
  let parent;
  let isContainer;
  const parentStateNode = parentFiber.stateNode;
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case FundamentalComponent:
      if (enableFundamentalAPI) {
        parent = parentStateNode.instance;
        isContainer = false; }}if (parentFiber.effectTag & ContentReset) {
    // Reset text content before insertion
    resetTextContent(parent);
    // Remove the ContentReset effectTag
    parentFiber.effectTag &= ~ContentReset;
  }

  // Find the base node
  const before = getHostSibling(finishedWork);

  // Perform the insert operation
  if (isContainer) {
    // Insert on the external DOM node
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    // Insert directly at the parent nodeinsertOrAppendPlacementNode(finishedWork, before, parent); }}Copy the code

One thing to be clear about here is where the DOM node is inserted, which is to find the corresponding parent based on the type of destination node.

If it is a HostRoot or HostPortal type node, first they have no corresponding DOM node, and second they will render the DOM child node to the corresponding external node (containerInfo) during actual rendering. So when the fiber node type is these two, insert the node into the external node, i.e. :

// Assign parent to containerInfo on fiber
parent = parentStateNode.containerInfo

...

// Insert it into the external node (containerInfo)
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent)
Copy the code

If it is a HostComponent, insert it directly into its parent DOM node, i.e

// Insert directly at the parent node
insertOrAppendPlacementNode(finishedWork, before, parent);
Copy the code

We see that when we actually insert, there’s a before involved, so what does that do?

Locating reference node

React to insert nodes, two kinds of circumstances, the newly inserted DOM node in it whether the location of the insert have siblings, no, execute parentInstance. The appendChild (child), so, BeforeChild call parentInstance. InsertBefore (child). This beforeChild is the beforeChild, which is the base node of the newly inserted DOM node so that it can be inserted into the correct position when the parent DOM node already has children. Considers if there has been a child node with parentInstance. Also the appendChild (child) to insert the new node to the insert to the end? This is clearly not true. So it is important to find the location of before, which is located via getHostSibling.

Let’s use an example to illustrate how getHostSibling works:

P is the newly generated DOM node. A is an existing DOM node with no change. Their positions in the Fiber tree are as follows. P needs to be inserted into the DOM tree. We can infer the final DOM tree form from this Fiber tree.

Fiber div # the DOM tree root div # root | | < App / > div | \ div/p a / ↖ ↖ Placement - > p -- -- -- -- > < Child / > | aCopy the code

As you can see, in the Fiber tree, A is the sibling of the parent node of P, while in the DOM tree, P and A are siblings, and P is inserted before A at the end.

According to the above diagram, we can deduce the process:

P has siblings<Child/>, it has child node A, a is a native DOM node, and A already exists in the DOM tree, so a is returned as the result, and P is inserted before A.

In another example, p is also a newly inserted node, and H1 exists in the DOM tree as an existing node.

Fiber div # the DOM tree root div # root | | < App / > div | / \ div p h1 / ↖ ↖ < Child1 / > -- > < Child2 / > | | | | Placement - > p h1Copy the code

P has no siblings. Go up<Child1/>, it has sibling nodes<Child2/>.<Child2/>Not a native DOM node<Child2/>H1 is a native DOM node and h1 already exists in the DOM tree. Then h1 is returned as the result and P is inserted before H1.

After two examples, the process of getHostSibling finding the sibling DOM node of the newly inserted node can be summarized as follows:

  1. Search sibling nodes first and filter out native DOM components.
  2. If not, look for the children of the peer node to filter out the native DOM component.
  3. The process of repeatedly finding siblings and then children until no more siblings can be found.
  4. Look up to the parent node, and repeat the first three steps to the parent node.
  5. Until the native DOM node is filtered out, if the DOM node is not the one to insert, it is returned as the result, i.e. locatedbefore(base node), the new node needs to be inserted in front of it.

There are the following rules:

Node to be insertedIf you have a sibling fiber node that is a native DOM node, it must be inserted before the node. If the sibling node is not a native DOM node, then it and the children of the sibling nodeSibling nodes at the DOM levelThe relationship between.

Node to be insertedIf there is no sibling node, then it and its parent’s sibling are childrenSibling nodes at the DOM levelThe relationship between.

The base node and the target node are siblings in the DOM tree and must be positioned after the target node

Next, take a look at the source code as summarized above:

function getHostSibling(fiber: Fiber): ?Instance {

  let node: Fiber = fiber;
  siblings: while (true) {

    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        // The parent of the new node is a DOM node.
        // Then it will be inserted into the parent node as the only node
        return null;
      }
      // If the parent node is not null and is not a native DOM node, keep looking
      node = node.return;
    }

    // Find the reference node from the sibling node
    node.sibling.return = node.return;
    node = node.sibling;

    // If a node is not one of the following types, it is definitely not a base node.
    // Because the base node requirements are DOM nodes
    // The loop will continue until the node is dom fiber.
    // Once the loop starts, node must not be the fiber that was originally passed in
    while( node.tag ! == HostComponent && node.tag ! == HostText && node.tag ! == DehydratedFragment ) {if (node.effectTag & Placement) {
        // If this node is also being inserted, continue the Siblings loop to find its reference node
        continue siblings;
      }
      if (node.child === null || node.tag === HostPortal) {
        // Node has no child node, or a HostPortal node is encountered, continue the siblings loop,
        // Find its base node.
        // Notice that the siblings loop starts with the code above
        // Check if the node has siblings. If the node has siblings, check if the node has siblings.
        continue siblings;
      } else {
        // Can't filter out the native DOM node, but it has child nodes.node.child.return = node; node = node.child; }}if(! (node.effectTag & Placement)) {// Filter out the native DOM node, and this node does not need to move,
      // stateNode is returned as the base node
      returnnode.stateNode; }}}Copy the code

At this point, the base node has been found, and the insert operation is performed.

Insert the node

Node insertion is a DOM tree operation. In addition to inserting the target node, it is also necessary to traverse its Fiber subtree to ensure that all the sub-DOM nodes are inserted. The traversal process is depth-first.

We will insert parent node insertOrAppendPlacementNode function is given priority to, to comb the insertion process.

Div # Fiber tree root | < App / > | div # App | Placement - > Child / > < / p -- -- -- -- -- - > span - h1 | aCopy the code

Now you would have a Child / > < insert into div # App, real insertion process is to first find div # App as a parent, again to find if there is a (Child / > < node, and then call insertOrAppendPlacementNode to perform inserts. Let’s see how it is called:

insertOrAppendPlacementNode(finishedWork, before, parent);
Copy the code

There are three parameters:

  • FinishedWork: is the fiber node that needs to be inserted<Child/>
  • Before:<Child/>, null in this scenario
  • Parent:<Child/>The parent node ofdiv#App

If finishedWork is a native DOM node, then the node will be inserted depending on whether there is before. Either way, the DOM will actually be inserted in the right place.

What if it’s not a native DOM node, like
, and you can’t insert it? Down from its child, called again insertOrAppendPlacementNode, namely recursively calls itself, the child will be a single not insert into the parent. In this example, p is inserted into parent.

At this time the Child nodes of the Child / > < insert has been completed, then will find p again brother nodes span, to insert it, and found that span and siblings h1, the h1 is inserted.

This is the complete process of node insertion. A feature similar to the insert node in the completeWork is that only the first-level child DOM node of the target node is inserted into the correct position, because the DOM nodes below the child DOM node have already been inserted while processing that layer.

To compare with the above process see insertOrAppendPlacementNode source code

function insertOrAppendPlacementNode(node: Fiber, before: ? Instance, parent: Instance,) :void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
    // If it is a native DOM node, insert directly
    const stateNode = isHost ? node.stateNode : node.stateNode.instance;
    if (before) {
      // Insert before the base node
      insertBefore(parent, stateNode, before);
    } else {
      // Insert it under the parent nodeappendChild(parent, stateNode); }}else if (tag === HostPortal) {
    // Does the HostPortal node do nothing
  } else {
    // Not a native DOM node, find its children
    const child = node.child;
    if(child ! = =null) {
      // Insert the child node
      insertOrAppendPlacementNode(child, before, parent);
      // Then find the sibling node
      let sibling = child.sibling;
      while(sibling ! = =null) {
        // Insert sibling nodes
        insertOrAppendPlacementNode(sibling, before, parent);
        // Continue to check sibling nodessibling = sibling.sibling; }}}}Copy the code

In the recursive call insertOrAppendPlacementNode insert node also introduced into before, this is before the beginning of the target node to insert the reference node. Let’s use two scenarios from the source code to see what this means.

Assuming the target node is not a native DOM node and has siblings of the existing DOM (i.e., the base node before, span) :

  • You have child nodes, you insert them into divs, and the final DOM form is the DOM tree on the right. P is not the same as span in the Fiber tree, but it is at the DOM level, so you have to insert it in front of span, which is the meaning of before in this kind of scene
Fiber div the DOM tree div | / \ Placement - > < Child / > -- -- -- -- > span / \ | span p | pCopy the code
  • After the child node is inserted, in the DOM tree finally formed, P, A and SPAN are brothers. P and A are inserted before span in sequence, so this kind of scene also needs before.
Fiber div the DOM tree div | | \ / Placement - > < Child / > -- -- -- -- > span / | to \ | p a span | p -- -- -- -- > aCopy the code

conclusion

It is not difficult to summarize the characteristics of node insertion process in COMMIT phase based on the problems caused by actual node insertion:

  1. Locate the correct DOM node insertion position
  2. Avoid redundant insertion of DOM nodes

Finding the reference node before is the key of point 1. With the reference node, we can know whether the parent node to be inserted has existing child nodes after the target node. The insertion strategy is determined based on the presence or absence of a base node.

How do you avoid unnecessary insertions of DOM nodes? As mentioned in the previous analysis of the insertion process, only the first-level child DOM nodes of the target node will be inserted in the correct position, because the child DOM node has been inserted. This is related to the order in which the fiber nodes are collected in the effectList. Since the collection is bottom-up, the fiber order is also bottom-up, resulting in bottom-up insertion of DOM nodes.

As you can see from the final effectList, the lowest node is first:

The above is the process of inserting DOM nodes according to Fiber tree.