React diff algorithm source code analysis

React diff algorithm source code analysis

The Diff algorithm in React is also called the reconciliation algorithm. The corresponding function is called reconcileChildren . Its main function is to mark those elements that have changed during the update process. These changes include addition, movement, and deletion. The process occurs in beginWork phase, and Diff is only performed when it is not the initial rendering.

I have read some articles before that describe the Diff algorithm as a comparison of two Fiber trees. This is incorrect. The actual Diff process is a comparison of a set of existing Fiber nodes and a new ReactElement generated by JSX , and then a new Fiber node is generated. In this process, the existing Fiber nodes are also tried to be reused.

Node Diff is divided into two types:

  1. Single node Diff - Element , Portal , string , number .
  2. Multi-node Diff - Array , Iterator .

The following React version is 17.0.1, and the code file is ReactChildFiber.old.js.

Single Node Diff

Single-node Diff is relatively simple. It will only try to reuse the node if the key and type are the same, otherwise it will return a new node.

In most cases, we will not assign keys to single nodes, so they default to null, which is the same.

reconcileSingleElement

  // Single node comparison function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    // The key of the new reactElement
    const key = element.key;
    //Current child fiber node let child = currentFirstChild;
    while (child !== null) {
      // Only diff if the key is the same
      if (child.key === key) {
        switch (child.tag) {
          // ...
          default: {
            // If the current fiber and reactElement have the same type, then if (child.elementType === element.type) {
              // Delete other nodes at the same level deleteRemainingChildren(returnFiber, child.sibling);
              // Reuse the current child fiber
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              // Return a reusable child fiber
              return existing;
            }
            break;
          }
        }
        // Unmatched delete nodes deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // Delete the node directly if the key is different deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    // New Fiber node const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }

Multi-Node Diff

The source code divides multiple nodes into array nodes and iterable nodes.

if (isArray(newChild)) {
  return reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
  );
}

if (getIteratorFn(newChild)) {
  return reconcileChildrenIterator(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
  );
}

The corresponding Diff functions are reconcileChildrenArray and reconcileChildrenIterator . Their core Diff logic is the same, so only the Diff of array nodes is analyzed - the reconcileChildrenArray function.

This section of code is relatively long, but the logic is very clear, and it is divided into two rounds of traversal from the dividing line.

  • The first round of traversal is the nodes with the same order and the same key, which need to be updated.
  • The second round of traversal is the nodes with different order and possibly different keys. These nodes need to be added, moved or deleted.

The first round of traversal is only for the case where the key and order are the same. The positions of the nodes corresponding to these keys have not changed, and only an update operation is required. Once the traversal encounters a different key, it is necessary to jump out of the loop.

// Old node <li key="0"/>
<li key="1"/>
<li key="2"/>
// New node <li key="0"/>
<li key="1"/>
<li key="5"/>

// key="5" is different, jump out of traversal // The first round of traversal node <li key="0"/>
<li key="1"/>
// <li key="2"/> and <li key="5"/> are left in the second round of traversal and comparison.

After the first round of traversal, there are two situations.

  1. The number of new nodes is less than the number of old nodes. At this time, the redundant old nodes need to be marked for deletion.
  2. The number of new nodes is greater than the number of old nodes. In this case, the extra new nodes need to be marked as newly added.

The second round of traversal is for different keys or different orders. The possible situations are as follows:

// Old node <li key="0"/>
<li key="1"/>
<li key="2"/>
// New node <li key="0"/>
<li key="2"/>
<li key="1"/>

// The second round of traversal compares the two nodes <li key="2"/> and <li key="1"/>

The second round of traversal will be a little more complicated, which will be discussed in detail later.

The detailed code is as follows.

reconcileChildrenArray

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    // Fiber node returned by the function let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    // oldFiber is a linked list let oldFiber = currentFirstChild;
    // Used to determine whether the node is moved let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // In the first round of traversal, only nodes with the same key are traversed for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        // Each time the old fiber node is looped, it will point to the sibling element, which is the fiber node of the next loop nextOldFiber = oldFiber.sibling;
      }
      // If the key is the same, return the fiber node; if the key is different, return null
      // If the type is the same, reuse the node, otherwise return a new node const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      // newFiber is null, indicating that the key is different, and the loop will be exited if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      // If newFiber.alternate is null, it is a new node, indicating that a new fiber node is created with a different type if (oldFiber && newFiber.alternate === null) {
        // Need to delete the oldFiber tag deleteChild(returnFiber, oldFiber);
      }
      // Place the node and update lastPlacedIndex
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // Form a new fiber node chain if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    /*
    After the first round of traversal, the number of new nodes is less than the number of old nodes. newChildren has been traversed and the remaining fiber nodes are deleted. The possible situation is as follows⬇️
    Before <li key="0"/>
    <li key="1"/>
    <li key="2"/>
    New <li key="0"/>
    <li key="1"/>
    <li key="2"/> will be deleted*/
    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    /*
    After the first round of traversal, the number of new nodes is greater than the number of old nodes. OldFiber has been traversed. The possible situation is as follows⬇️
    Before <li key="0"/>
    <li key="1"/>
    New <li key="0"/>
    <li key="1"/>
    <li key="2"/>
    A new <li key="2"/> will be added. This section is the insertion logic of the new node*/
    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        // Form a new fiber node chain if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }
      
    // ---------------------------------------------------------------------

    // Use the remaining oldFiber to create a key->fiber node Map, so that the corresponding old fiber node can be obtained by key const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    
    // Second round of traversal, continue to traverse the remaining nodes, these nodes may need to be moved or deleted for (; newIdx < newChildren.length; newIdx++) {
      // Get the old node corresponding to the key from the map and return the updated new node const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        // Reuse the new node and delete the old node from the map. The corresponding situation may be a change in position if (newFiber.alternate !== null) {
          // The reused nodes need to be removed from the map, because the remaining nodes in the map will be marked as Deletion existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
        // Place the node and determine whether to move lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    // Delete the remaining useless nodes existingChildren.forEach(child => deleteChild(returnFiber, child));

    return resultingFirstChild;
  }

The first round of traversal is relatively easy to understand. Here we will analyze the second round of traversal in detail, because the second round will raise the question of whether reuse needs to be moved.

The second round of traversal first traverses the remaining oldFiber and forms a key -> 舊fiber節點的Map , which can be used to quickly obtain old nodes through keys.

The following traversal still uses the new node as the traversal object. Each time the traversal uses the key of the new node to take out the old node from the Map to compare whether it can be reused. The corresponding function is updateFromMap .

If the node has an alternate attribute, it is a reused node. At this time, it needs to be removed from existingChildren . Subsequently, the nodes that still exist in existingChildren after the second round of traversal will be marked as deleted.

How to determine if a node has moved?

There is a variable lastPlacedIndex here to determine whether the node has moved. This value is updated every time a node is added to a new Fiber linked list.

When oldIndex of the reused node is less than lastPlacedIndex , it is moved. If it does not need to be moved, lastPlacedIndex will be updated to the larger oldIndex , and the next node will be judged by the new value. The code is as follows:

function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number,
): number {
  newFiber.index = newIndex;
  const current = newFiber.alternate;
  if (current !== null) {
    const oldIndex = current.index;
    if (oldIndex < lastPlacedIndex) {
 			// Node movement newFiber.flags = Placement;
      return lastPlacedIndex;
    } else {
      // The node position has not changed return oldIndex;
    }
  } else {
    // Insert new node newFiber.flags = Placement;
    return lastPlacedIndex;
  }
}

For example:

// old abcd
// New acbd

abcd are all key values.

After the first round of traversal, the remaining nodes need to be compared:

// Old bcd
// New cbd

Node a has been reused in the first round and placeChild has been called. At this time, the lastPlacedIndex value is 0.

Entering the second round of traversal, the new node is still the traversal object.

c => exists in the old node and can be reused. Its index in the old node is 2. 2 > lastPlacedIndex(0). No need to move. Assign lastPlacedIndex to 2.
b => exists in the old node and can be reused. Its index in the old node is 1, 1 < lastPlacedIndex(2). It needs to be moved and marked with Placement.
d => exists in the old node and can be reused. Its index in the old node is 3, 3 > lastPlacedIndex(2), so it does not need to be moved.

From this example, we can see that in React, the nodes on the right that do not need to be moved are used as references, and the nodes that need to be moved are all moved uniformly from left to right.

In the subsequent Layout stage, the nodes marked with Placement will be insertBefore .

Summarize

The core code of the Diff algorithm in React is not very long, but the introduction of key cleverly changes the complexity from O(n3) to O(n).

The internal competition among programmers is too serious, so I have to learn the source code.

The above is the detailed content of the source code analysis of the react diff algorithm. For more information about the react diff algorithm, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • Do you know the Diff algorithm in React?
  • In-depth analysis of the diff algorithm in React
  • Detailed explanation of virtual DOM and diff algorithm in react
  • A brief discussion on analyzing the Diff algorithm from the React rendering process
  • Example implementation of React diff algorithm
  • React diff algorithm implementation ideas and principle analysis

<<:  Detailed explanation of the usage of the alias command under Linux

>>:  How to solve "Unable to start mysql service error 1069"

Recommend

DOM operation implementation in react

Table of contents Previous words Usage scenarios ...

How to use pdf.js to preview pdf files in Vue

When we preview PDF on the page, some files canno...

UTF-8 and GB2312 web encoding

Recently, many students have asked me about web p...

jQuery achieves the shutter effect (using li positioning)

This article shares the specific code of jQuery t...

Join operation in Mysql

Types of joins 1. Inner join: The fields in the t...

vue+ts realizes the effect of element mouse drag

This article example shares the specific code of ...

How to automatically delete records before a specified time in Mysql

About Event: MySQL 5.1 began to introduce the con...

Element dynamic routing breadcrumbs implementation example

To master: localStorage, component encapsulation ...

How to get the current time using time(NULL) function and localtime() in Linux

time(); function Function prototype: time_t time(...

Cross-origin image resource permissions (CORS enabled image)

The HTML specification document introduces the cr...

JavaScript dynamically generates a table with row deletion function

This article example shares the specific code of ...

How to implement scheduled backup of MySQL in Linux

In actual projects, the database needs to be back...

A detailed discussion of evaluation strategies in JavaScript

Table of contents A chestnut to cover it Paramete...