The code has been linked to github: link address if you feel good, you can click on the star, here will continue to share their experience in development (:

Virtual DOM

What is the virtual DOM

A programming concept that corresponds to a real DOM node, an object that represents the real DOM.

React virtual DOM

React belongs to ReactElement and has the following format:

const reactElement = {
  key:null.props: {className:"".onClick: () = > {},
  	children:[]
  },
  type:'div'
}
Copy the code

Code creation: use React. CreateElement or JSX

React.createElement('div', {className:"".onClick: () = >{}},//child...
])
Copy the code

DOM slow? Virtual DOM fast?

The two are not simple generalizations, because the virtual DOM ultimately manipulates the real DOM.

  • Real DOM operations are slower than native JS apis, such as group operations.
  • Any DOM-based library (React, Vue), which manipulates the DOM, is no faster than the real DOM.
  • When there are fewer normal nodes, the virtual DOM will render faster than the real DOM (not faster to operate, but shorter to not interact with), while when there are too many nodes, such as 10W, the real DOM will operate faster.

So why do people say DOM is slow and virtual DOM is fast? Because in some cases, the virtual DOM is actually faster than the real DOM. First, we need to understand the concept that JS computation is faster than dom manipulation

  1. Reduce DOM manipulation
    1. Combine DOM operations to reduce DOM operations. (Add 1000 nodes, calculate 1000 times, insert once)
    2. Using DOM diff, the virtual DOM can reduce the scope of DOM operations by eliminating unnecessary operations (add 1000 nodes, only 10 are new after comparison).
  2. cross-platform

The virtual DOM is essentially a JS object, so it can be converted into components like applets, Android views, and so on.

React and JS control the difference between native DOM

Events (such as Click)

  • JS click events listen on the corresponding DOM, of course, can also be directly delegated to the parent element through the event delegate,
  • React click events are listened on the virtual DOM, while React events are synthesized events, which can be understood as events delegated to the root node

Attributes (such as class style)

  • JS directly to modify the corresponding attribute
  • React triggers rerender, domDiff, merge and modify properties by changing props or state.

DOM diff

What is DOM Diff

The process of generating a new Fiber node by comparing the currently updated component to the corresponding Fiber node in the last rendering (commonly known as the Diff algorithm).

DOM Diff strategy

  1. Diff only sibling elements. React does not attempt to reuse a DOM node if it crosses the hierarchy in two updates.
  2. Two components with the same class will generate similar tree structures, and two components with different classes will generate different tree structures, so whenever the type of the current node changes, React destroys its descendant nodes and creates new descendant nodes.
  3. For the same level of array nodes, each child node can pass uniquekeyMake a distinction.

DOM Diff processes are fully parsed

The new Diff entry function for reconcileChildFibers, which is part of the React Fiber task cycle, runs during the Fiber tree building phase.

This function compares the old Fiber node with the new ReactElement object and returns the new fiber node:

The React package location: packages/React – the reconciler/SRC/ReactChildFiber. Js

function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
) :Fiber | null {
  const isObject = typeof newChild === 'object'&& newChild ! = =null;
  if (isObject) {
    // The object type is handled according to the element type
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
      // Calls the reconcileSingleElement to process the reconcileSingleElement
      / /... omit}}if (typeof newChild === 'string' || typeof newChild === 'number') {
    The reconcileSingleTextNode is called
    / /... omit
  }

  if (isArray(newChild)) {
    The reconcileChildrenArray is called to process the reconcileChildrenArray
    / /... omit
  }

  // Some other cases call handlers
  / /... omit

  // If none of the above matches, delete the node
  return deleteRemainingChildren(returnFiber, currentFirstChild);

}
Copy the code

As can be seen from the source code, DOM Diff will use different comparison algorithms according to the node type, of course, mainly in the case of single node and multi-node, we will analyze the two diff algorithms respectively below:

Single node Diff

Taking the case of whether to reuse a React element in the Object reconcileSingleElement for example, the Diff reconcileSingleElement comes into the reconcileSingleElement, and the overall logic is:

  • According to thekeyAnd element typestypeAre consistent
    • If they are all the same, real DOM nodes can be reused (stateNode),
    • If inconsistent, mark that the fiber node needs to be removed and return the newly generated fiber node

This function compares the old Fiber node with the new ReactElement object, and returns the new fiber node.

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ) :Fiber {
    const key = element.key;
    let child = currentFirstChild;
    // Check whether the current node exists
    while(child ! = =null) {
      // TODO: If key === null and child.key === null, then this only applies to
      // the first item in the list.
      // The key is the same
      if (child.key === key) {
        switch (child.tag) {
					/ /... Omit case, the core is to determine whether the tag is the same, the same type indicates that it can be reused
          if (child.elementType === element.type) {
            // All other sibling elements are marked for deletion
            deleteRemainingChildren(returnFiber, child.sibling);
            // Clone the current fiber with the new properties and return
            const existing = useFiber(child, element.props);
            returnexisting; }}// The key is the same, but the tag is different. Subsequent sibling nodes do not need to be traversed, and both themselves and their sibling nodes are marked as deleted
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // If the key is different, the current node cannot be reused and the flag is deleted
        deleteChild(returnFiber, child);
      }
      // Traverses all sibling nodes of the same level
      child = child.sibling;
    }

    // omit creating a new Fiber and return
  }
Copy the code

As you can see, the single-node Diff code is relatively simple. The only tricky point is that sibling nodes are marked as deleted in two cases:

  • Reuse node, mark other sibling nodes to delete
  • keySame thing, buttypeThe difference is that the tag itself and other sibling nodes are removed

This is because the old Fiber tree renders multiple nodes, while the new one only has a single node. For example, there were three nodes before, but only one node is updated. When one node is reused, the other two nodes will be marked as deleted. Similarly, if a node with the same key cannot be reused, then of course all nodes should be marked as deleted.

Multi-node Diff

React is a bit more complicated than single-node Diff. Based on the higher frequency of DOM updates in the daily interface, React improves the priority of judging updates and divides Diff into two traversal stages:

  • The first round is traversed to determine whether the node can be reused
    • If reusable, continue traversing
    • If it’s not reusable,keyDifferences lead to directThe end of the traverse;typeThe current fiber node needs to be deleted to continue traversal
    • Until the new React element arraynewChildrenWalk through, or the old Fiber nodeoldFiberThe traversal is complete.
  • The second iteration will be handled according to the different circumstances of the first iteration
    • newChildrenoldFiberIf the traversal is complete, only the update is performed.
    • newChildrenAfter traversal,oldFiberIf the traversal is not complete, it indicates that the node needs to be deleted here, so the remaining nodes need to be traversedoldFiberNode, marked for deletion.
    • newChildrenI’m not done,oldFiberIf the traversal is complete, a new node needs to be inserted, and the old nodes have been reused, so the remaining nodes need to be traversednewChildrenGenerate a new Fiber node.
    • newChildrenandoldFiberNone of them have been traversed, indicating that some nodes have changed their positions and need to be moved in this update, which is also difficult to understand the Diff. We will analyze the Diff with detailed examples in the future.

For a multi-node Diff reconcileChildrenArray, it compares the old Fiber node with the new ReactElement array, and returns the first Fiber node in the array.

The React package location: packages/React – the reconciler/SRC/ReactChildFiber. Js

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ) :Fiber | null {

    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    //== The first iteration, newChildren
    for(; oldFiber ! = =null && newIdx < newChildren.length; newIdx++) {
      // The pointer of the old fiber is moved backwards to ensure the element index is consistent between traversals
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // Compare index new and old, return null if the key is different
      UseFiber (update logic) if the types are the same; createXXX(insert logic) if the types are different
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        // If null is returned, the key is different and the node is not updated
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      // This is not the first time to create fiber, shouldTrackSideEffects is set to true
      if (shouldTrackSideEffects) {
        // A new node is created. The old node needs to be marked as deleted
        if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); }}// lastPlacedIndex Index of the position of the last reusable node in oldFiber
      // If the current node is reusable, determine whether the location is moved.
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // Create a new fiber list
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    // oldFiber nodes need to be deleted, so the remaining oldFiber nodes need to be traversed, marked delete.
    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    // oldFiber traversal is complete, newChildren traversal is not complete, indicating that new nodes need to be inserted, and old nodes have been reused, so we need to traversal the remaining newChildren to generate new fiber nodes.
    if (oldFiber === null) {
			/ /... Omit this part of the code
      // Notice that as in the first pass, if the current node is reusable, there is also a judgment whether the position is moved.
      return resultingFirstChild;
    }

    //== Loop 2, loop newChildren

    // Convert the old fiber to map, which is easy to find using key
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Walk through the rest of newChildren to find reusable nodes
    for (; newIdx < newChildren.length; newIdx++) {
      // Check whether the old node is reusable by key
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if(newFiber ! = =null) {
        if (shouldTrackSideEffects) {
          if(newFiber.alternate ! = =null) {
            existingChildren.delete(
              newFiber.key === null? newIdx : newFiber.key, ); }}// As in the first pass, if the current node is reusable, then determine whether the position is moved.
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else{ previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; }}if (shouldTrackSideEffects) {
      // newChildren has been traversed, so the remaining nodes in oldFiber sequence are deemed to be deleted (mark Deletion).
      existingChildren.forEach(child= > deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }
Copy the code
Resolution of node movement

Our goal is to find the moving node, so we need to clarify: does the node move what is the reference? React uses the lastPlacedIndex index for the position of the last reusable node in oldFiber.

Because updates are traversed according to newChildren, during traversal, if there is no movement, oldIndex of the current React element’s reusable node is always larger than lastPlacedIndex of the last reusable node. So if the current React element uses a smaller index, oldIndex, than lastPlacedIndex, it means that the updated old node has been moved to the right.

Here we follow the source code analysis:

  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number.) :number {
    // New fiber node updated index
    newFiber.index = newIndex;
    if(! shouldTrackSideEffects) {// Noop.
      return lastPlacedIndex;
    }
    // Get the reusable node fiber
    const current = newFiber.alternate;
    if(current ! = =null) {
      const oldIndex = current.index;
      // The previous position index of the reusable node is less than the position index to be inserted for this update, indicating that the node needs to be moved (to the right)
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        newFiber.flags = Placement;
        return lastPlacedIndex;
      } else {
        // This item can stay in place.
        // The previous location index of the reusable node is greater than or equal to the location index to be inserted in this update, indicating that the node does not need to be moved
        // If oldIndex >= lastPlacedIndex, lastPlacedIndex = oldIndex
        returnoldIndex; }}else {
      // This is an insertion.
      // There are no reusable nodes
      newFiber.flags = Placement;
      returnlastPlacedIndex; }}Copy the code

Simple text and source code may still be difficult to understand, let’s take an example step by step analysis:

/ / before update
abcd

/ / updatedAbdc == First iteration == newChildren === ABDC oldFiber === ABCD lastPlacedIndex ===0A (before) vs a(after) is reusable oldIndex =0Is equal to lastPlacedIndex, don't move it, update lastPlacedIndex = oldIndex =0B (before) vs b(after) is reusable oldIndex =1, greater than lastPlacedIndex, no need to move, update lastPlacedIndex = oldIndex =1C (before) vs D (after) key changes, so it's not reusable, so I'm done iterating, and lastPlacedIndex =1

/ /.. The middle is omitted because deletion and insertion are not performed== newChildren == dc oldFiber === CD lastPlacedIndex ===1Comparison d oldFiber exists, reusable oldIndex =3Greater than lastPlacedIndex, you don't have to move it, lastPlacedIndex = oldIndex =3Compare c oldFiber existing, reusable oldIndex =2Is less than the last place index, so I need to move it, the last place index stays the same, or3NewChildren traversal completed == Finally == ABD three nodes do not move, c node moved to the right after DCopy the code

Abcd => ABDC. React actually locates the order of ABD first, and then c moves to the left. This also reminds us to avoid moving nodes forward. React will keep nodes in the same position and move nodes in front of it to the right, which costs performance. For example, abcd=>dabc. It would look like D would move first, but React would position D and ABC would move backwards.

If you are still confused or want to debug the source code for further understanding, you can go to github.com/wqhui/react… Download debugging.

The DOM example Diff

The Key role

  1. Two child elements, delete the latter (if there is no key).

    The tag type and tag attributes remain unchanged and do not need to be updated. The child element is changed from [1,2] to [2], the tag of 1 remains unchanged, but the children element is updated (the content of child element 2 is put here); The child element 2 is missing. Delete the corresponding DOM.

  2. Two child elements, delete the latter (in the case of a key).

The tag type and tag attributes remain unchanged and do not need to be updated. The child element changes from [1,2] to [2], but because there is a key, the computer knows that the element key:1 has been deleted, and 2 remains unchanged, so it will directly delete 1 and keep 2.

reference

The React algorithm of harmonic react.iamkasong.com/diff/multi….