I. Description of the phenomenon

Echarts has recently been used in vUE pages to dynamically add divs to the top of the page based on user clicks, and then draw the corresponding line graph within it. Instead of appearing in the right place, the image is always drawn in the top div, as shown.

The expected result should be

DOM structure (without keys) :

<div style="display: flex;">
    <div v-for="item in divArr" :id="item.name">      
    </div>
</div>
Copy the code

Unshift ({name: ‘order’, chart: undefined}); unshift({name: ‘order’, chart: undefined}); The value of name is the ID of the newly added div. Get the node with the newly added ID in nextTick, initialize the Echarts object, and draw the graph.

this.$nextTick((id) = > {
    let chart = this.$echarts.init(document.getElementById(id))
    chart.setOption(option)
    divArr[0].chart = chart   // Multiple clicks in a short period of time are not considered
})
Copy the code

When four different buttons are clicked, the situation shown in Figure 1-4 appears. Using the console to print the echarts objects generated by each click, you can see that the DOM nodes of the Echarts objects are different, but the IDS are the same.

Presumably, the first div is reused without a key, and subsequent divs are newly added.

Adding a key yields the expected result:

This leads to thinking about the role of key in V-FOR.

Now, let’s dig into the source code to verify and understand why the result is different after binding the key.

Two, source code analysis

Because divArr is treated as a reactive object, page updates are triggered when divArr changes.

vm.$el = vm.__patch__(prevVnode, vnode)
Copy the code

For the rendering process and vDOM do not understand the children can refer to vue2 different types of Watcher comparison on the render Watcher introduction, as well as vue2 vDOM and diff algorithm these two articles.

Let’s take a look at the path process when the page is updated (listing the logic for this scenario) :

function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (sameVnode(oldVnode, vnode)) {   / / perform patch
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
    } else {   / / cover
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        // create new node
        createElm( vnode, 
                   insertedVnodeQueue, 
                   oldElm._leaveCb ? null : parentElm,
                   nodeOps.nextSibling(oldElm)
        ) 
    }
    return vnode.elm
}
Copy the code
function sameVnode (a, b) {
  return  a.key === b.key && a.tag === b.tag
}
Copy the code

When no key is bound, the key of oldVnode and newVnode is undefined and the tag is div, so sameVnode(oldVnode, newVnode) is valid. Go to patchVode –> updateChildren diff comparison.

patchVode

view code
Function patchVnode(){const oldCh = oldvNode. children const ch = vnode.children props, style if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, If (isUndef(vnode.text)) {if (isDef(oldCh) &&isdef (ch)) {if (oldCh! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (process.env.NODE_ENV ! == 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } } else if (oldVnode.text ! == vnode.text) { nodeOps.setTextContent(elm, vnode.text) } }Copy the code

The updateAttrs method (SRC \platforms\ Web \ Runtime \modules\attrs.js) performs an update to the attribute:

function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  / /...
  let key, cur, old
  const elm = vnode.elm
  const oldAttrs = oldVnode.data.attrs || {}
  let attrs: any = vnode.data.attrs || {}
  // clone observed objects, as the user probably wants to mutate it
  / /...
  // Add/update attributes
  for (key in attrs) {
    cur = attrs[key]
    old = oldAttrs[key]
    if(old ! == cur) { elm.setAttribute(key, cur) } }// Delete attributes that do not exist on the new node
  for (key in oldAttrs) {
    if(! (keyin attrs)) {
        elm.removeAttribute(key)
    }
  }
}
Copy the code

updateChildren

view code

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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 const canMove = ! removeOnly if (process.env.NODE_ENV ! == 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode,  newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) 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) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) }Copy the code

Copy the code

}

Double-ended comparison is mainly used here, and then recursive patchVnode is performed on children, which is also referred to as peer comparison. Let’s take a look at the diff process using the previous two clicks.

  1. Compare nodes WHOSE IDS are I and II. SameVnode (oldStartVnode, newStartVnode) is established. Run patchVnode -> updateAttrs to update node IDS. OldStartIdx and newStartIdx add 1
  2. OldStartIdx <= oldEndIdx is not true, exit the while loop. In this case, if ref is null, run addVnodes to add the second node of newVnode, that is, the node whose ID is I.
  function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }
Copy the code

Therefore, the problem raised at the beginning of the article is precisely because after the second click, the node with the original ID of I is reused locally and its ID is updated as II. The new node with ID I is newly added, so the graph previously drawn is not retained, but replaced on the node with ID II.

What happens when you bind the key?

  1. Due to different keys, sameVnode(oldStartVnode, newStartVnode) is not valid. Therefore, sameVnode(oldStartVnode, newEndVnode) is valid. Then execute oldStartVnode and patchVnode of newEndVnode. OldStartIdx plus 1, newEndIdx minus 1.
  2. OldStartIdx <= oldEndIdx is not true, exit the while loop. If newEndIdx = 0 and ref = newCh[newEndIdx + 1]. Elm, insert node II before node I.

Therefore, binding keys in v-for loops can more accurately locate vNodes during diff, reducing unexpected errors.