React source code analysis 9. Diff algorithm

Time:2021-10-20

React source code analysis 9. Diff algorithm

Video course (efficient learning): enter the course

Course Directory:

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

When updating the fiber node in the render phase, we will call reconcilechildfibers to compare the current fiber and JSX objects to build a workinprogress fiber. Here, current fiber refers to the fiber tree corresponding to the current DOM, and JSX is the return value of the class component render method or function component.

In reconcilechildfibers, single node diff or multi node diff will be entered according to the type of newchild

//ReactChildFiber.old.js
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {

  const isObject = typeof newChild === 'object' && newChild !== null;

  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
                //Single node diff
        return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
    }
  }
    //...
  
  if (isArray(newChild)) {
     //Multi node diff
    return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
  }

  //Delete node
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

The main flow chart of diff process is as follows:

React source code analysis 9. Diff algorithm

We know that the complexity of comparing the two trees is O (N3), which is unbearable for our application. React puts forward three preconditions to reduce the complexity:

  1. Only peer comparison, cross level DOM will not be reused
  2. Different types of nodes generate different DOM trees. In this case, the old nodes and descendant nodes will be directly destroyed and new nodes will be created
  3. You can provide reusable clues to the process of element diff through key, for example:

    const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
    const b = (
      <>
        <p key="1">1</p>
        <p key="0">0</p>
      </>
    );

If the elements in a and B do not have keys, because the text nodes before and after the node update are different, they cannot be reused. Therefore, the previous node will be destroyed and a new node will be created. But now there is a key, the node in B will find the node with the same key in the old a and try to reuse it. Finally, it is found that the update can be completed only by changing the location, The specific comparison process will be described later.

Single node diff

Single point diff has the following conditions:

  • The same key and type indicates that nodes can be reused
  • Key: directly mark and delete the node, and then create a new node
  • If the key is the same but the type is different, mark to delete the node and the sibling node, and then create a new node
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  //The child node is not null to perform comparison
  while (child !== null) {

    //1. Compare key
    if (child.key === key) {

      //2. Comparison type

      switch (child.tag) {
        //...
        
        default: {
          if (child.elementType === element.type) {
            //If the type is the same, the reused node can be reused
            return existing;
          }
          //Jump out of different types
          break;
        }
      }
      //If the key is the same and the type is different, delete the fiber and brother fiber tags
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      //Key: delete the node with different marks
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }
   
  //New fiber
}

Multi node diff

Multi node diff is complex. We discuss it in three cases, where a represents the node before update and B represents the node after update

  • Attribute change

    const a = (
        <>
          <p key="0" name='0'>0</p>
          <p key="1">1</p>
        </>
      );
      const b = (
        <>
          <p key="0" name='00'>0</p>
          <p key="1">1</p>
        </>
      );
  • Type change

    const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
      const b = (
        <>
          <div key="0">0</div>
          <p key="1">1</p>
        </>
      );
  • New node

    const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
      const b = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
          <p key="2">2</p>
        </>
      );
  • Node deletion

    const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
          <p key="2">2</p>
        </>
      );
      const b = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
  • Node position change

        const a = (
        <>
          <p key="0">0</p>
          <p key="1">1</p>
        </>
      );
      const b = (
        <>
          <p key="1">1</p>
          <p key="0">0</p>
        </>
      );

In the source code, multi node diff has three for loop traversals (it does not mean that all updates go through three traversals. Entering the loop body is conditional and jumping out of the loop is conditional). The first traversal handles node updates (including props updates and type updates and deletions), and the second traversal handles other situations (node additions). The reason is that in most applications, The frequency of node update is more frequent, and the node setting of the third processing bit changes

  • First traversal

    Because the old node exists in current fiber, it is a linked list structure. Remember the fiber dual cache structure. Nodes are connected through child, return and sibling, while new children exist in JSX. When traversing the comparison, first compare newchildren [i] ` with ` oldfiber, and then let I + +, nextoldfiber = oldfiber.sibling. In the first round of traversal, three cases will be processed, of which the first and second cases will end the first cycle
    1. Key is different. The first cycle ends
    2. Newchildren or oldfiber traversal is completed, and the first cycle ends
    3. The key is different from the type, and the oldfiber is marked as deletion
    4. If the key is the same and the type is the same, it can be reused

    After the newchildren traversal, the oldfiber has not been traversed. After the first traversal, the nodes in the oldfiber that have not been traversed are marked as deletion, that is, the deleted deletion tag

  • Second traversal
    The second traversal considers three cases

    1. Both newchildren and oldfiber are traversed: the multi node diff process ends

      1. Newchildren are not traversed, but oldfiber is traversed, and the remaining nodes of newchildren are marked as placement, that is, the inserted tag

        1. If newchildren and oldfiber are not traversed, they enter the node movement logic
  • Third traversal
    The main logic is in the placechild function. For example, the node order is ABCD before updating and acdb after updating

    1. The A and key of the first position in newchild and oldfiber are the same and reusable, lastplacedindex = 0

      1. The C in the second position of newchild is different from the B and key in the second position of oldfiber. Jump out of the first cycle and save the BCD in oldfiber in the map
      1. The index of C at the second position in newchild in oldfiber = 2 > lastplacedindex = 0. No need to move, lastplacedindex = 2
      2. The index of D at the third position in newchild in oldfiber = 3 > lastplacedindex = 2. There is no need to move, lastplacedindex = 3
      3. B at the fourth position in newchild has index = 1 < lastplacedindex = 3 in oldfiber and moves to the last

    Look at the picture more intuitively

    React source code analysis 9. Diff algorithm

    For example, the node order before updating is ABCD and after updating is DABC

    1. The D of the first position in newchild and the A and key of the first position in oldfiber are different and cannot be reused. Save the ABCD in oldfiber in the map with lastplacedindex = 0

      1. The index of D at the first position in newchild in oldfiber = 3 > lastplacedindex = 0 does not need to be moved, lastplacedindex = 3
      1. Index = 0 < lastplacedindex = 3 of a in the second position of newchild in oldfiber, moving to the last
      2. Index = 1 < lastplacedindex = 3 in oldfiber for B at the third position in newchild, moving to the last
      3. The index of C at the fourth position in newchild in oldfiber = 2 < lastplacedindex = 3, moving to the last

    Look at the picture more intuitively

    React source code analysis 9. Diff algorithm

The code is as follows

//ReactChildFiber.old.js

function placeChild(newFiber, lastPlacedIndex, newIndex) {
       newFiber.index = newIndex;
   
       if (!shouldTrackSideEffects) {
         return lastPlacedIndex;
       }
   
    var current = newFiber.alternate;
 
       if (current !== null) {
         var oldIndex = current.index;
   
         if (oldIndex < lastPlacedIndex) {
           //If oldindex is less than lastplacedindex, the node is inserted to the end
           newFiber.flags = Placement;
           return lastPlacedIndex;
         } else {
           return oldIndex;// No need to move lastplacedindex = oldindex;
         }
       } else {
         //New insert
         newFiber.flags = Placement;
         return lastPlacedIndex;
       }
     }
//ReactChildFiber.old.js

function reconcileChildrenArray(
    Returnfiber: fiber, // parent fiber node
    Currentfirstchild: first node in fiber | null, // children
    Newchildren: array < * >, // the new node array is JSX array
    Lanes: lanes, // Lane related Chapter 12 introduction
  ): Fiber | null {

    let resultingFirstChild: Fiber | null = null;// The first node returned after diff
    let previousNewFiber: Fiber | null = null;// Last compared node in the new node

    let oldFiber = currentFirstChild;// Oldfiber being compared
    let lastPlacedIndex = 0;// Last reusable node location or oldfiber location
    let newIdx = 0;// Compared position in new node
    let nextOldFiber = null;// Oldfiber being compared
    For (; oldfiber! = = null & & newidx < newchildren. Length; newidx + +) {// first traversal
      If (oldfiber. Index > newidx) {// nextoldfiber assignment
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      Const newfiber = updateslot (// update the node. If the keys are different, newfiber = null
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;// Jump out of the first traversal
      }
      If (shouldtracksideeffects) {// check shouldtracksideeffects
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// Tag node insertion
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);// Mark the nodes in oldfiber that have not been traversed as deletion
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      For (; newidx < newchildren. Length; newidx + +) {// the second traversal
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// Insert new node
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    //Add the remaining oldfiber to the map
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    For (; newidx < newchildren. Length; newidx + +) {// process node movement for the third cycle
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            Existingchildren. Delete (// delete the found node
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// Logic marked for insertion
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      //Delete the remaining nodes in existingchildren
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }