15 pictures, 20 minutes to understand the core principle of Diff algorithm, I said!!!

preface

Hello, everyone, in the daily interview, Diff algorithm is a barrier that can't be bypassed. In the most popular words, speaking about the most difficult knowledge points has always been the purpose of my article. Today I'll explain Diff algorithm in a popular way? Lets Go  

  What is virtual DOM

Before talking about Diff algorithm, let me tell you what virtual DOM is. This is conducive to a deeper understanding of the Diff algorithm.

Virtual DOM is an object. What kind of object is it? An object used to represent the real DOM, remember this sentence. Let me give you an example. Look at the following real DOM:

<ul id="list">
    <li class="item">ha-ha</li>
    <li class="item">ha-ha</li>
    <li class="item">hey</li>
</ul>

The corresponding virtual DOM is:

let oldVDOM = { // Old virtual DOM
        tagName: 'ul', // Tag name
        props: { // Label properties
            id: 'list'
        },
        children: [ // Signature sub node
            {
                tagName: 'li', props: { class: 'item' }, children: ['ha-ha']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['ha-ha']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['hey']
            },
        ]
    }

At this time, I modify one   li tag   Text for:  

<ul id="list">
    <li class="item">ha-ha</li>
    <li class="item">ha-ha</li>
    <li class="item">Lin Sanxin hahaha</li> // modify
</ul>

Generated at this time   New virtual DOM   Is:  

let newVDOM = { // New virtual DOM
        tagName: 'ul', // Tag name
        props: { // Label properties
            id: 'list'
        },
        children: [ // Signature sub node
            {
                tagName: 'li', props: { class: 'item' }, children: ['ha-ha']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['ha-ha']
            },
            {
                tagName: 'li', props: { class: 'item' }, children: ['Lin Sanxin hahaha']
            },
        ]
    }

This is what we usually call the old and new virtual DOMS. At this time   New virtual DOM   Is the latest status of the data, so let's take it directly   New virtual DOM   To render into   Real DOM   Is it really more efficient than directly operating a real DOM? It certainly won't. look at the figure below:  

From the above figure, we can see that the second method must be faster, because there is one in the middle of the first method   Virtual DOM   Steps, so   Virtual DOM is faster than real dom   This sentence is actually wrong, or not rigorous. What is the correct statement? Virtual DOM algorithm operates real DOM, and its performance is higher than that of directly operating real dom. Virtual Dom and virtual DOM algorithm are two concepts. Virtual DOM ALGORITHM = Virtual DOM + Diff algorithm

What is Diff algorithm  

We said above   Virtual DOM, I also know that only   Virtual DOM + Diff algorithm can really improve performance. That's all   Virtual DOM, let's talk about it again   Diff algorithm   Let's go back to the above example (the compressed image is a little small. You can open it and see it clearly):  

In the figure above, in fact, only one li tag modifies the text, and the others remain unchanged. Therefore, it is not necessary to update all nodes. Just update this li tag. Diff algorithm is the algorithm to find this li tag.

Summary: Diff algorithm is a comparison algorithm. Compare the old virtual Dom and the new virtual DOM, compare which virtual node has changed, find out the virtual node, and only update the real node corresponding to the virtual node without updating the node whose other data has not changed, so as to accurately update the real Dom and improve efficiency.

Loss calculation using virtual DOM algorithm: total loss = Virtual DOM addition, deletion and modification + (related to the efficiency of Diff algorithm) real DOM difference addition, deletion and modification + (fewer nodes) typesetting and redrawing

Loss calculation of direct operation of real DOM: total loss = complete addition, deletion and modification of real DOM + (there may be more nodes) typesetting and redrawing

  Principle of Diff algorithm

  Diff same layer comparison

When comparing new and old virtual DOM S, Diff algorithm will only be compared at the same level, not cross level. So Diff algorithm is: depth first algorithm.   Time complexity: O(n)

Diff comparison process  

When the data changes, the setter will be triggered and all subscribers will be notified of the Watcher through Dep.notify. The subscribers will call the patch method to patch the real DOM and update the corresponding view. For those who don't know much about this step, you can take a look at what I wrote before Vue source code series

newVnode and oldVnode: old and new virtual nodes in the same layer

patch method

This method is used to compare whether the current virtual nodes on the same layer are labels of the same type (the standards of the same type will be described below):

  • Yes: continue to execute the patchVnode method for deep comparison
  • No: there is no need to compare. Replace the whole node with   New virtual node

Let's take a look at the core principle code of patch

function patch(oldVnode, newVnode) {
  // Compare whether it is a type of node
  if (sameVnode(oldVnode, newVnode)) {
    // Yes: continue to conduct in-depth comparison
    patchVnode(oldVnode, newVnode)
  } else {
    // no
    const oldEl = oldVnode.el // Real DOM node of old virtual node
    const parentEle = api.parentNode(oldEl) // Get parent node
    createEle(newVnode) // Create a real DOM node corresponding to the new virtual node
    if (parentEle !== null) {
      api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // Add new element to parent element
      api.removeChild(parentEle, oldVnode.el)  // Remove previous old element nodes
      // Set null to free memory
      oldVnode = null
    }
  }

  return newVnode
}

sameVnode method

The key step of patch is   The sameVnode method determines whether it is a node of the same type. The problem arises. How can it be regarded as a node of the same type? this   type   What are the criteria for?

Let's have a look   sameVnode   The core principle code of the method is clear at a glance

function sameVnode(oldVnode, newVnode) {
  return (
    oldVnode.key === newVnode.key && // Is the key value the same
    oldVnode.tagName === newVnode.tagName && // Is the tag name the same
    oldVnode.isComment === newVnode.isComment && // Are all annotation nodes
    isDef(oldVnode.data) === isDef(newVnode.data) && // Are data defined
    sameInputType(oldVnode, newVnode) // When the tag is input, must the type be the same
  )
}

patchVnode method

This function does the following:

  • Find the corresponding   Real DOM, called el
  • judge   newVnode   and   oldVnode   Whether to point to the same object. If so, directly   return
  • If they all have text nodes and are not equal, then   el   The text node of is set to   newVnode   Text node for.
  • If   oldVnode   There are child nodes and   newVnode   If not, delete   el   Child nodes of
  • If   oldVnode   Without child nodes   newVnode   If yes, it will   newVnode   The child nodes of are added to el after they are materialized
  • If both have child nodes, execute   updateChildren   Function to compare child nodes, which is very important
  • function patchVnode(oldVnode, newVnode) {
      const el = newVnode.el = oldVnode.el // Get real DOM object
      // Gets the child node array of the old and new virtual nodes
      const oldCh = oldVnode.children, newCh = newVnode.children
      // Terminate if the old and new virtual nodes are the same object
      if (oldVnode === newVnode) return
      // If the old and new virtual nodes are text nodes and the text is different
      if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {
        // The text in the real DOM is directly updated to the text of the new virtual node
        api.setTextContent(el, newVnode.text)
      } else {
        // otherwise
    
        if (oldCh && newCh && oldCh !== newCh) {
          // Both old and new virtual nodes have child nodes, and the child nodes are different
    
          // Compare child nodes and update
          updateChildren(el, oldCh, newCh)
        } else if (newCh) {
          // The new virtual node has child nodes, but the old virtual node does not
    
          // Create a child node of the new virtual node and update it to the real DOM
          createEle(newVnode)
        } else if (oldCh) {
          // The old virtual node has child nodes, but the new virtual node does not
    
          //Directly delete the corresponding child nodes in the real DOM
          api.removeChild(el)
        }
      }
    }
    

    The other points are well understood. Let's talk about them in detail   updateChildren  

    updateChildren method  

    This is   patchVnode   The most important method in is to compare the child nodes of the old and new virtual nodes, which occurs in the updateChildren method. Next, let's talk about it in combination with some figures to make you better understand it

    What is a comparison method? namely   Head and tail pointer method  , The new child node set and the old child node set have two pointers at the beginning and end, for example:

    <ul>
        <li>a</li>
        <li>b</li>
        <li>c</li>
    </ul>
    
    After modifying data
    
    <ul>
        <li>b</li>
        <li>c</li>
        <li>e</li>
        <li>a</li>
    </ul>
    
    

    Then the new and old child node sets and their head and tail pointers are:  

     

    Then they will compare with each other. There are five comparison situations:

  • 1. oldS and newS use   sameVnode method for comparison, sameVnode(oldS, newS)
  • 2. oldS and newE use   Compare the sameVnode method, sameVnode(oldS, newE)
  • 3. oldE and newS use   sameVnode method for comparison, sameVnode(oldE, newS)
  • 4. oldE and newE use   sameVnode method for comparison, sameVnode(oldE, newE)
  • 5. If none of the above logics can match, replace all the old child nodes    key   Make a mapping to the subscript of the old node   key -> index   Table, and then use the new   vnode   of   key   To find the location that can be reused in the old node.
  •  

    Next, take the above code as an example to analyze the comparison process

    Before analysis, please remember that the final rendering result should be subject to newVDOM, which also explains why subsequent node movements need to be moved to the corresponding position of newVDOM

     

      First step

     

    oldS = a, oldE = c
    newS = b, newE = a
    

    Comparison results: oldS and newE   Equal, you need to move node a to the position corresponding to newE, that is, the end, and oldS + +, newE--  

     

    Step 2  

    oldS = b, oldE = c
    newS = b, newE = e

    Comparison result: oldS and news are equal. You need to   Node b   Move to   newS   At the same time, oldS++,newS++  

     

    Step 3  

    oldS = c, oldE = c
    newS = c, newE = e
    

    Comparison result: oldS, oldE and news are equal, so it is necessary to   Node c   Move to   newS   At the same time, oldS++,newS++  

     

    Step 4  

    Olds > olde, then   oldCh   The traversal is complete first, and   newCh   I haven't finished traversing. It means   There are more newch than oldch, so you need to insert the extra nodes into the corresponding positions on the real DOM  

     

    I'll leave you a thinking question here. The example above is   Newch is more than oldch. If the opposite is true, yes   Oldch ratio   If there are many new ch, that is   newCh   Go through the cycle first, and then   oldCh   There will be more nodes. As a result, these old nodes will be deleted in the real DOM. You can think for yourself and simulate this process. Like me, drawing and simulation can consolidate the above knowledge.

    enclosed   updateChildren   Core principles of   code

    function updateChildren(parentElm, oldCh, newCh) {
      let oldStartIdx = 0, newStartIdx = 0
      let oldEndIdx = oldCh.length - 1
      let oldStartVnode = oldCh[0]
      let oldEndVnode = oldCh[oldEndIdx]
      let newEndIdx = newCh.length - 1
      let newStartVnode = newCh[0]
      let newEndVnode = newCh[newEndIdx]
      let oldKeyToIdx
      let idxInOld
      let elmToMove
      let before
      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == null) {
          oldStartVnode = oldCh[++oldStartIdx]
        } else if (oldEndVnode == null) {
          oldEndVnode = oldCh[--oldEndIdx]
        } else if (newStartVnode == null) {
          newStartVnode = newCh[++newStartIdx]
        } else if (newEndVnode == null) {
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
          patchVnode(oldStartVnode, newStartVnode)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
          patchVnode(oldEndVnode, newEndVnode)
          oldEndVnode = oldCh[--oldEndIdx]
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
          patchVnode(oldStartVnode, newEndVnode)
          api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
          oldStartVnode = oldCh[++oldStartIdx]
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
          patchVnode(oldEndVnode, newStartVnode)
          api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
          oldEndVnode = oldCh[--oldEndIdx]
          newStartVnode = newCh[++newStartIdx]
        } else {
          // Comparison when using key
          if (oldKeyToIdx === undefined) {
            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // index table generated with key
          }
          idxInOld = oldKeyToIdx[newStartVnode.key]
          if (!idxInOld) {
            api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
            newStartVnode = newCh[++newStartIdx]
          }
          else {
            elmToMove = oldCh[idxInOld]
            if (elmToMove.sel !== newStartVnode.sel) {
              api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
            } else {
              patchVnode(elmToMove, newStartVnode)
              oldCh[idxInOld] = null
              api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
      } else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
    

    Use index as key  

    usual   v-for   Why not use index as the key of the loop item when rendering in a loop?

    Let's take an example. The initial data is on the left, and then I insert a new data in front of the data to become a list on the right

    <ul>                      <ul>
        <li key="0">a</li>        <li key="0">Lin Sanxin</li>
        <li key="1">b</li>        <li key="1">a</li>
        <li key="2">c</li>        <li key="2">b</li>
                                  <li key="3">c</li>
    </ul>                     </ul>
    
    

      Logically speaking, the most ideal result is to insert only one li tag new node and leave the others unchanged to ensure the highest efficiency of DOM operation. But if we use index as the key here, will we really achieve our ideal result? Without much nonsense, practice it:

    <ul>
       <li v-for="(item, index) in list" :key="index">{{ item.title }}</li>
    </ul>
    <button @click="add">increase</button>
    
    list: [
            { title: "a", id: "100" },
            { title: "b", id: "101" },
            { title: "c", id: "102" },
          ]
          
    add() {
          this.list.unshift({ title: "Lin Sanxin", id: "99" });
        }
    

    Click the button and we can see that not the result we expected, but all li tags have been updated  

     

    Why? Or through the diagram to explain

    Logically, a, b, c   The three li tags are all before reuse, because they have not changed at all. What has changed is that a new Lin Sanxin has been added in front

     

    But as we said earlier, we are in the process of child node   diff algorithm   In the process, the   Comparing the sameNode of the old head node with the sameNode of the new head node, this step hits the logic, because now the old and new head nodes are used twice   of   key   All   0. Similarly, the nodes with 1 and 2 keys also hit the logic, resulting in the node with the same key   patchVnode   Update the text, which is already there   c node  , However, because there was no node with key 4 before, it was regarded as a new node, so it was funny. Using index as the key, the last new node was the existing c node. So the first three are carried out   patchVnode   Update the text, and the last one is updated   newly added  , That explains why all li tags have been updated.

    How can we solve it? In fact, we just need to use a unique value as the key  

    <ul>
       <li v-for="item in list" :key="item.id">{{ item.title }}</li>
    </ul>
    

    Now let's see the effect  

     

    Why do we use id as the key to achieve our ideal effect? Because if we do so, the keys of nodes a, b and c will never change. The keys before and after the update are the same, and because the contents of nodes a, b and c have not changed, it is even changed   patchVnode also does not perform complex update operations inside, saving performance. However, Lin Sanxin node is treated as a new node and added to the real DOM because there is no node corresponding to its key before updating.


    epilogue  

     

    I hope it can help those students who have always wanted to understand virtual DOM and Diff algorithm

    If you think this article can help you a little, please give me a praise, ha ha

    You are welcome to point out my mistakes and I will correct them in time

     

     

Tags: Vue.js Algorithm

Posted on Fri, 17 Sep 2021 11:27:13 -0400 by mburkwit