preface

First, let’s take a look at how the Vue website describes the function of the key attribute.

The special attribute of key is mainly used in the virtual DOM algorithm of Vue to identify VNodes when comparing old and new nodes. If keys are not used, Vue uses an algorithm that minimizes dynamic elements and tries to modify/reuse the same type of elements in place whenever possible. With a key, it rearranges the elements based on the change in the key and removes elements where the key does not exist.

Just look at the above description, and you’re probably like me, still not sure what it means. I’ll show you some examples of what the key property can do.

V – for the key

Let’s start with an example:

<template> <div> < button@click ="refresh()"> refresh </button> <ul> <li V -for="item in array">{{item.name}} <input type="checkbox" /></li> </ul> </div> </template> <script> export default { name: 'Demo', data() { return { array = [ { name: '11', key: '11' }, { name: '22', key: '22' }, { name: '33', key: '33' }, ] } }, methods: { refresh() { this.array = [ { name: '00', key: '00' }, { name: '11', key: '11' }, { name: '22', key: '22' }, { name: '33', key: '33' }, ] } } } </script>Copy the code

Check the checkbox of the first element and click refresh button. Insert a new element in the header of the array. Shouldn’t it follow the element whose name is 11?

This is because we didn’t declare the key property. Here we analyze the reasons for the result according to the Vue VNode update principle:

When the VNode virtual DOM is updated, the old and new VNode structures of the above components look something like this:

// old vnode
const oldVnode = {
  tag: 'div',
  children: [
    { //... el-button },
    {
    	tag: 'ul',
        children: [
            {
    		tag: 'li',
      		children: [
      			{
    				text: '11'
    			},
                        {
                                tag: 'input'
                        },
      		]
            },
            {
    		tag: 'li',
      		children: [
      			{
    				text: '22'
                        },
                        {
                                tag: 'input'
                        },
      		]
            },
            {
    		tag: 'li',
      		children: [
      			{
                                text: '33'
                        },
                        {
                                tag: 'input'
                        },
      		]
            }
      ]
    }
  ]
}
Copy the code
// new vnode
const newVnode = {
  tag: 'div',
  children: [
    { //... el-button },
    {
    	tag: 'ul',
        children: [
            {
    		tag: 'li',
      		key: undefined,
      		children: [
      			{
                                text: '00'
                        },
                        {
                                tag: 'input'
                        },
      		]
            },
            {
    		tag: 'li',
      		key: undefined,
      		children: [
      			{
                                text: '11'
                        },
                        {
                                tag: 'input'
                        },
      		]
            },
            {
                tag: 'li',
                key: undefined,
      		children: [
      			{
                                text: '22'
                        },
                        {
                                tag: 'input'
                        },
      		]
    	   },
           {
                tag: 'li',
                key: undefined,
      		children: [
      			{
                                text: '33'
                        },
                        {
                                tag: 'input'
                        },
      		]
            }
      ]
    }
  ]
}
Copy the code

When updating children of ul elements, the Vue internally performs the updateChildren function to compare all children of the same level, using a two-ended comparison algorithm.

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 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

The logic of the sameVnode function to determine whether two nodes are the same is as follows:

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

If the key attribute is not declared, the newly inserted 00 node is considered to be the same as the original 11 node. The update process is as follows:

Three times of patchVnode process and one node creation process were executed. In each patchVnode, text node update is performed. Because the checkbox control is not modified, it is not updated, and the previous content is reused in place, so the state of the previous node is preserved.

When we declare the key attribute, its execution logic is as follows:

The first time the vNode head is equal to the old one, the result is false, so start with the tail node. Three times of patchVnode process and one node creation process were also executed in the above process. However, every time in patchVnode, no DOM operation is performed, because the label content is not modified. Newly inserted nodes are also added to the DOM in the corresponding order. Unlike the undeclared key, the new node is not the new element 00 as we thought, but 33.

Therefore, using keys in V-for improves the rendering speed of components and allows components to be reordered as expected without in-place reuse. (Note: Using index as key has the same effect as not using key, so do not use index as key.)

V-if uses keys to manage reusable elements

Here’s an example from the official website if you allow users to switch between different login methods:

<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username"> </template>  <template v-else> <label>Email</label> <input placeholder="Enter your email address"> </template>Copy the code

Switching loginType in the code above will not clear what the user has entered. Because both templates use the same element, the will not be replaced — just its placeholder.

This is not always practical, so Vue gives you a way to say “These two elements are completely separate, don’t reuse them”. Simply add a key attribute with a unique value:

<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address" key="email-input">
</template>
Copy the code

The input box will now be re-rendered each time you switch.

Note that

Switching the same label element triggers a transition effect

<template> <transition mode=" out-of-in "> <button V-if ="show" @click="show = false"> </button> <button V-else @click="show" </button> </transition> </template> <script> export default {name: 'Demo', data() {return {show: true, } }, } </script> <style scoped> .v-enter-active, .v-leave-active { transition: all 1s; } .v-enter, .v-leave { opacity: 0; } </style>Copy the code

When the key attribute is not used, the interaction is as follows:

At this time, the transition effect is not triggered, because when the button is switched, the previous button will be reused, only the text of the button is updated, so there is no new and deleted elements involved, resulting in the transition effect is not triggered.

After adding the key attribute:

<template> <transition mode=" out-of-in "> <button V-if ="show" @click="show = false" key="show"> display </button> <button V-else @click="show = true" key="hide"> </button> </transition> </template> <script> export default {name: 'Demo', data() { return { show: true, } }, } </script> <style scoped> .v-enter-active, .v-leave-active { transition: all 1s; } .v-enter, .v-leave { opacity: 0; } </style>Copy the code

The animation executes normally.

conclusion

  • Using keys in V-for improves the rendering speed of components and allows components to be reordered as expected without in-place reuse. (Note: Using index as key has the same effect as not using key, so do not use index as key.)
  • You can use keys in V-IF to manage reusable elements.
  • Using the key attribute, you can solve the problem that the transition effect is not triggered when the same label element is switched.