preface

Questions about the diff algorithm of vue, patch, vnode, and why to add key, key can not be index, are related issues, and these questions are aggregated together. The whole process of vUE was formed by converting the real DOM into VDOM and finding the smallest update according to key comparison through diff algorithm, and then modifying the patch accordingly and adding the real DOM.

This article addresses the problem

  1. The entire DOM update process for vUE
  2. What is the diff
  3. What is a patch
  4. Key and why index can’t be used as key

Demo code

When the code comes out, when you review the elements, you color the first one and the last one, like this, for the last problem, check for index as the key.

<div id="app"> < button@click ="del"> Index) in the list ": the key =" index "> {{` this is the first item ` ${item}}} < / div > < / div > < script > new Vue ({el: '# app, the data () {return {list: [1, 2, 3] } }, methods: { del() { this.list.splice(0, 1); } } }) </script>Copy the code

First of all, a brief overview of vUE initialization is shown in the figure

The whole diagram roughly shows some of the basic operations done during initialization. When the first update was made, there was no old virtual node, so the corresponding diff process was not performed to the core. In the source code, the old node did not create elements directly.

if (isUndef(oldVnode)) {
   createElm(vnode, insertedVnodeQueue)
} 
Copy the code

What happens when you hit the delete button

Update -> watcher.run -> update -> update -> update -> update. Thus, patchVnode can be made.

When deleting the first operation, extract some of the source code as follows,

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
        const oldCh = oldVnode.children
        const ch = vnode.children
        if (isUndef(vnode.text)) {
          if (isDef(oldCh) && isDef(ch)) {
            // The new and old nodes have children, and the children are not the same, then diff through the new and old lists to compare.
            if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if (isDef(ch)) {
            // The child nodes of both old and new vNodes exist.
            // The child of the new node exists, but the child of the old node does not
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ' ')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) {
            // The child of the new node does not exist. The child of the old node exists, indicating that the child node is deleted
            removeVnodes(oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
            // Clear the text, the text of the old node exists, the text of the new node does not exist
            nodeOps.setTextContent(elm, ' ')}}else if(oldVnode.text ! == vnode.text) {// The new and old nodes are text nodes, and the text has changed, update the text node
              nodeOps.setTextContent(elm, vnode.text)
          }
        }
    }
    
Copy the code

After the operation of patchVnode is performed, if both the old and new nodes have child nodes, the updatechildren operation is performed. It can also be seen that the comparison process of VUE is peer comparison

Diff process for child nodes

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // Four indexes are defined
    // New start node, new end node, old start node, old end node;
    let oldStartIdx = 0
    let 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, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    constcanMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
      checkDuplicateKeys(newCh)
    }

    // Iterate over the old and new groups of nodes
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // If a node is moved, it may not exist on the current index, detect this, and adjust the index if the node does not exist
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // If the new and old nodes are the same, run the patch command
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // Update new and old index + 1 after patch ends
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // The new and the old are the same node
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // Index -1 after patch ends
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // The new node and the old node start the same node and execute patch
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // The new node is the same as the old one
        // Move the node to the end of all unprocessed nodes in oldChildren, since it is new, move the node to the end of the unprocessed node to be last
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // Move to the front of all unprocessed oldChildren nodes
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // If none of the above assumptions is true, find the index of the position of the new node in the old node by traversing

        {key1: idx1,... }
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // Find the index of the new start node's position in the old node in the map
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          // If no new start node is found in the old node, it is a newly created element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // The new start node is found in the old node
          vnodeToMove = oldCh[idxInOld]
          // If the two nodes are the same, perform patch
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // Patch ends. The old node is set to undefined
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            // In the last case, the node is found, but the two nodes are not the same node, the new element is considered, and the creation is performed
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }

    // The old node or the new node has been traversed
    if (oldStartIdx > oldEndIdx) {
      // If the old nodes are traversed and the new nodes are available, the remaining nodes are newly added
      refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // If the new node is traversed but the old node is available, the node is deleted
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
Copy the code

And the idea here is, because we’re going through the list of children of the old and the new nodes, and we’re comparing them in pairs, we’re doing an optimization here, which is, we’re constantly reducing the length of the traversal, rather than comparing them directly, we’ve set up four different types of nodes: new head, old head, new tail, old tail, and we’re comparing them first

Scenario: Considering that in most business scenarios, not all child node positions will change, the following comparison methods are known to reduce the number of cycles

  • New head compared to old tail
  • Compare the new tail with the old head
  • New head compared to old head
  • New tail compared to old tail

According to the code, when the comparison is the same, the operation of patchVnode will be carried out and the corresponding subscript will be changed at the same time, which can solve The Times of traversal well. If none of the above is the case, create a mapping between key and index in the old node by createKeyToOldIdx, and then find the corresponding key in the new node. If the new key does not exist, create and delete the patchVnode

Finally, after the comparison, if the old node still has surplus, it means that the remaining nodes are newly added. If the new node has surplus, it means that it is a new element, new element.

After analyzing the basic patch and diff flow, let’s see what happens when the first item in the list is removed

  1. One is executed firstpatchVnodeMethod, and then compare the executionupdateChildrenKey parts of the generated VNode are extracted as follows
[{tag: 'button', children: [{text: 'delete first item '}]}, {tag: 'div', key:0, children: [{text: 'This is the first1Item '}]}, {tag: 'div', key:1, children: [{text: 'This is the first2Item '}]} {tag: 'div', key:2, children: [{text: 'This is the first3Item '}]}] children: [{tag: 'button', children: [{text: 'delete first item '}]}, {tag: 'div', key:0, children: [{text: 'This is the first2Item '}]}, {tag: 'div', key:1, children: [{text: 'This is the first3Item '}]}]Copy the code

We ignore the patchVnode process of the button and only look at the result of the V-for loop. First, we compare the list items oldCh and CH by comparing them with updateChildren. We get two divs. So continue updateChildren diff operation, and then execute patchVnode, because it is a text node, so the logic is to directly set the text of the new node to the real DOM. The picture below shows the situation.

Then the next one will repeat the above process and implement it from there, like this

In this case, during the updateChildren execution, the new Vnode is iterated, and the node is deleted because the old Vnode is still iterated, and the situation occurs.

We observed a phenomenon

  1. Text deleted correctly,
  2. Dom deletion is incorrect

Because of this, we add color to the element when we review it so we can see if it was a deletion error.

Because we set the key to index, the first two items in the new and old VNodes are considered to be the same node. PatchVnode is executed and the text is changed during this process. After updateChildren, the old node has surplus, so the last Vnode is deleted. This is a delete error, so you can’t use index as key. Index cannot be used as key. Cannot use index as key!!

Above, is the whole process, have seen many times, have seen a lot of relevant blogs, summed up, and also part of the understanding is not very clear, may be a few days later I read the reading again and again will find that I understand before from, may have a new understanding, and then watch, check, again understanding, slowly, after the repeated process probably will slowly progress.