React diff algorithm

  • Only peer nodes are compared, not reused if DOM nodes move across hierarchies
  • Use key to build a map of the old node, and delete it from the map after reuse
  • lastPlacedIndexRepresents the index of the last node not to be moved
  • The principle of movement is to move as little as possible, if there must be one to move, the new status of the higher motionless, the new status of the lower move

Compare the order

  • 1. If the node corresponding to the key can be found, then compare the type. If the type is different, delete the old node and create a new one.
  • LastPlacedIndex <= oldIndex does not need to be moved, otherwise it needs to be moved and updated

Change the execution order of A, B, C, D, E, F to A, C, E, B, G

  • lastPlacedIndex = 0
  • A exists in the map and is in the same position. Multiplexing nodes update attributes
  • C Compare lastPlacedIndex < oldIndex, lastPlacedIndex = 2
  • E compare lastPlacedIndex < oldIndex, lastPlacedIndex = 4
  • B: lastPlacedIndex > oldIndex
  • G is not found in map, need to create and insert
  • Mark the remaining elements D F in the map as deleted

Change the dom order: delete first, then update and move, and finally insert

const EFFECT_TYPE = {
  UPDATE: 2./ / update
  INSERT: 4./ / insert
  INSERT_UPDATE: 6.// Insert and update
  DELETE: 8./ / delete
};

const oldNodes = [{ key: "A" }, { key: "B" }, { key: "C" }, { key: "D" }];

const newNodes = [
  { key: "D" },
  { key: "A" },
  { key: "F" },
  { key: "G" },
  { key: "B"},];* A B C D becomes * D A F G B *1 A B D *2. B D A *3. D A B *4. D A F B *5. D A F G B */

function diff(oldNodes, newNodes) {
  let lastPlacedIndex = 0;
  const oldNodeMap = oldNodes.reduce((memo, v, i) = > {
    memo[v.key] = v;
    v.oldIndex = i;
    return memo;
  }, {});

  newNodes.forEach((newNode, newIndex) = > {
    const oldNode = oldNodeMap[newNode.key];
    if(! oldNode) {/ / couldn't find it
      newNode.effectTag = EFFECT_TYPE.INSERT;
      newNode.insertIndex = newIndex;
      return;
    }

    // Delete from map
    delete oldNodeMap[newNode.key];

    // Insert and update at different positions
    if (lastPlacedIndex <= oldNode.oldIndex) {
      // The index is greater than lastPlacedIndex
      newNode.effectTag = EFFECT_TYPE.UPDATE;
      lastPlacedIndex = oldNode.oldIndex;
    } else {
      // Index less than lastPlacedIndex needs to be movednewNode.effectTag = EFFECT_TYPE.INSERT_UPDATE; }});return Object.keys(oldNodeMap);
}

const deletions = diff(oldNodes, newNodes);

console.log("Delete node", deletions);

console.log(newNodes);
Copy the code

Vue diff algorithm

The DIff algorithm in VUE involves logic to manipulate the DOM, so use HTML for the demonstration

  • OldNodes represents the old list of virtual nodes, el- real DOM, tag- tag type
  • The domDiff method takes three arguments, el- the parent of the node to be compared, oldChildren- the old virtual DOM list, and newChildren- the new virtual DOM list
  • Node comparison in VUE uses double Pointers, traversing from both ends to the middle. When the Pointers cross, the comparison is completed
  • At the beginning of traversal, the comparison of head to head, tail to tail, head to tail and tail to head is carried out successively, which is also an optimization point of DIff algorithm in VUE
  • All of these are compared, and then compare the other nodes that do not move regularly

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul id="container">
      <li id="domA">A</li>
      <li id="domB">B</li>
      <li id="domC">C</li>
      <li id="domD">D</li>
    </ul>
    <script>
      const oldNodes = [
        { key: "A".el: domA, tag: "li" },
        { key: "B".el: domB, tag: "li" },
        { key: "C".el: domC, tag: "li" },
        { key: "D".el: domD, tag: "li"},];const newNodes = [
        { key: "C".tag: "li" },
        { key: "A".tag: "li" },
        { key: "F".tag: "li" },
        { key: "G".tag: "li" },
        { key: "B".tag: "li"},];/** * see if the two nodes are the same, and compare the tags and keys *@param {*} newVnode
       * @param {*} oldVnode* /
      function isSameVnode(newVnode, oldVnode) {
        return newVnode.tag === oldVnode.tag && newVnode.key == oldVnode.key;
      }

      / * * * *@param {*} El Real DOM node *@param {*} OldChildren old virtual dom *@param {*} NewChildren New virtual DOM */
      function domDiff(el, oldChildren, newChildren) {
        // Old start index
        let oldStartIndex = 0;
        // The old start node
        let oldStartVnode = oldChildren[0];
        // Old end index
        let oldEndIndex = oldChildren.length - 1;
        // The old end node
        let oldEndVnode = oldChildren[oldEndIndex];
        
        // New start index
        let newStartIndex = 0;
        // New start node
        let newStartVnode = newChildren[0];
        // The new end index
        let newEndIndex = newChildren.length - 1;
        // The new end node
        let newEndVnode = newChildren[newEndIndex];
        
        // Construct a map based on the old node
        let oldNodeMap = oldChildren.reduce((memo, item, index) = > {
          // A: 0 Records the position
          memo[item.key] = index;
          return memo;
        }, {});

        // Double pointer comparison, from both ends to the middle, when the pointer is crossed, the comparison is complete
        while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
          // When the pointer moves, the element may have already been removed, so skip this item
          if(! oldStartVnode) { oldStartVnode = oldChildren[++oldStartIndex];console.log("1. OldStartVnode is empty");
          } else if(! oldEndVnode) { oldEndVnode = oldChildren[--oldEndIndex];console.log("2. OldEndVnode empty");
          } else if (isSameVnode(oldStartVnode, newStartVnode)) {
            console.log("3. Same head", newStartVnode.key);
            // Compare the header, if the same move the header pointer
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
          } else if (isSameVnode(oldEndVnode, newEndVnode)) {
            console.log("4. Same from end to end", newEndVnode.key);
            // Compare tail to tail, if same, move tail pointer
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
          } else if (isSameVnode(oldStartVnode, newEndVnode)) {
            console.log(
              '5. Same head and tail movement${oldStartVnode.key}${oldEndVnode.key}Before the next node of '
            );
            // end comparison
            // Move the real DOM of oldStartvNode. el to the end of the old node
            el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
            oldStartVnode = oldChildren[++oldStartIndex];
            newEndVnode = newChildren[--newEndIndex];
          } else if (isSameVnode(oldEndVnode, newStartVnode)) {
            // end to end comparison
            console.log(
              '6. Same movement of tail head${oldEndVnode.key}${oldStartVnode.key}Before `
            );
            el.insertBefore(oldEndVnode.el, oldStartVnode.el);
            oldEndVnode = oldChildren[--oldEndIndex];
            newStartVnode = newChildren[++newStartIndex];
          } else {
            // All the above are special cases
            // Head/tail/head/tail/head/tail/head/tail
            // Compare out of order
            let moveIndex = oldNodeMap[newStartVnode.key];
            if (moveIndex === undefined) {
              // Can not find index, is a new node, need to create
              el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
              console.log('7. Create a new node${newStartVnode.key}Inserted into the${oldStartVnode.key}Before `);
            } else {
              / / found
              let moveVnode = oldChildren[moveIndex];
              el.insertBefore(moveVnode.el, oldStartVnode.el);
              // Mark the moved node as undefine
              oldChildren[moveIndex] = undefined;
              console.log(
                '8. Move the out-of-order node${moveVnode.key}${oldStartVnode.key}Before `); } newStartVnode = newChildren[++newStartIndex]; }}// Insert a new one
        if (newStartIndex <= newEndIndex) {
          / / reference
          let anchor =
            newChildren[newEndIndex + 1= = =null
              ? null
              : newChildren[newEndIndex + 1].el;
          for (let i = newStartIndex; i <= newEndIndex; i++) {
            console.log("Insert", newChildren[i].key); el.insertBefore(createElm(newChildren[i]), anchor); }}// The old one needs to be removed
        if (oldStartIndex <= oldEndIndex) {
          for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            let child = oldChildren[i];
            console.log("Delete", child.key); child && el.removeChild(child.el); }}}/** * Create a real DOM from the virtual DOM *@param {*} vnode
       * @returns* /
      function createElm(vnode) {
        let { tag, text, key } = vnode;

        if (typeof tag === "string") {
          vnode.el = document.createElement(tag);
          vnode.el.innerText = key;
        } else {
          vnode.el = document.createTextNode(text);
        }
        return vnode.el;
      }

      domDiff(container, oldNodes, newNodes);
    </script>
  </body>
</html>
Copy the code

The difference between

The same

  • Both groups of virtual DOM comparisons (Fiber vs. virtual DOM after React16.8)
  • Only the same nodes are compared, which simplifies the algorithm complexity
  • The old node will be reused only if the key and label type are the same
  • Before traversal, a map is constructed based on the old nodes for quick search by key

The difference between

  • React only records nodes that need to be modified during diff traversal, forming an Effect list. The actual DOM will be modified according to the Effect list. The actual DOM will be deleted first, then updated and moved, and finally inserted
  • Vue uses the real DOM for traversalinsertBeforeMethod, which modifies the real DOM, and finally deletes it
  • React uses a single pointer to traverse from left to right
  • Vue uses double Pointers to traverse from both ends to the middle
  • React’s virtual diff is relatively simple, with some optimization in VUE, which is relatively complex but more efficient