Problem description

Using keys in lists is a cliche, and official best practice strongly recommends using keys… No, the official quote is:

A key must always be used with v-FOR on a component to maintain the state of the internal component and its subtrees.

If what the authorities are saying is so pertinent, it must be important. However, I did not know the truth at the beginning, always feel that there is no difference between writing and not writing ah, anyway can render out. Write good trouble ~ 🥱, calculate, next time, next time must write…

The reason you can’t tell the difference is because everything happened so fast… It wasn’t until I hit a break point that I realized there was more than a little difference. Here’s the thing

I made A list like this, starting with five elements, A,B,C,D,E. Insert an element F into the head of the list two seconds after the component is mounted, and compare the difference between using key and not using key

<div id="app">
  <ul>
<! -- <li v-for="item in list" :key="item">{{item}}</li>-->
        <li v-for="item in list">{{item}}</li>
  </ul>
  <h2>Instead of using key, look closely at the changes in the above elements</h2>
</div>
Copy the code
  const app = new Vue({
    data() {
      return {
        list: ['A'.'B'.'C'.'D'.'E']}},el: '#app',
    mounted() {
      setTimeout((a)= > {
        this.list.unshift('F')},2000)}})Copy the code

Do not use the key

Use the key

The difference between

  • There are five updates and one new insert (element E) without the key
  • With key, only one new insert (element F) is done

Problem analysis

Why, you might be wise enough to ask?

As we know, virtual DOM is a tree structure. When component data changes, patchVnode method of component will be executed, and then diff will be conducted according to the depth-first and same-layer comparison principle. If both old and new nodes have children, the updateChildren method is called to compare the children, and the core algorithm of the virtual DOM Diff is in this method.

The virtual DOM patching algorithm in VUE is modified on the basis of SnapDOM, and the most important optimization includes web scenes. In Web, the most we can do is to insert elements at the end or the beginning of the team, and make blind guesses at the beginning and end of the new and old elements 2×2=4 times before traversing to find the old elements corresponding to the new elements.

I’ll insert comments in the source code below for ease of review

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

    constcanMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
      checkDuplicateKeys(newCh)
    }
    // If the cursors of any of the old and new elements overlap, the loop is terminated
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
        // For common operations on the Web, first make blind guess 2x2=4 times before traversing to find the old element corresponding to the new element
        // Compare the new beginning with the old one
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // Compare the new end with the old end
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        // Compare the new end with the old beginning
      } 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]
        // Compare the new beginning with the old end
      } 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 {
        // Create a mapping table for the index and key of the old node
        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 {
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // When the loop ends, if there are any unprocessed elements in the new list, all new elements are created
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // If there are unprocessed elements in the old list, delete them all
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
 }
Copy the code

The sameVnode method is used to execute a blind guess, so you can see the code directly.

// Check whether the two VNodes are the same
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
Copy the code

When we insert an F element at the beginning of a list, if we don’t use key, then key equals undefined, undefined===undefined, and both tags are Li, not comment nodes, Therefore, the newly inserted F (newStartVnode) and the old A (oldStartVnode) are considered to be the same node for updating. The cursor is moved back one bit, and the following four elements are updated according to this logic. Finally, the old node ends first (the cursor overlapped first), and the new node still has one element E. A new node is created and inserted, resulting in a total of five update operations and a new insert (element E).

If we use a key and we compare newStartVnode and oldStartVnode and find that the key is different and not the same element, we continue to compare oldEndVnode and oldStartVnode and find that they have the same key, E, and the text content and other attributes are the same. We just reuse it and move the cursor forward one bit. Finally, the old node ends first (the cursor overlapped first), and the new node has one element F left, so we create a new node and insert it. Only one new insert (element F) is performed

extended

In addition to improving performance, there are other usage scenarios for using keys, such as:

  1. When you want to animate an element, for example by moving it up and down, if you don’t use a key, the element is likely to be animatedupdateChildrenThe element’s animation may not be fully displayed
  2. When you get focus on an input, if you don’t use key, passupdateChildrenAfter the update, you may lose focus
  3. Other possible bugs caused by dom order changes

The most direct and effective way to solve the above problem is to add a key to the element PS: next time be sure to write key 😂

Finally, this article belongs to a series of articles from the source code, welcome everyone to pay attention to, message, clap brick