Vue 中 key 属性的作用

前言

首先,我们先看一下 Vue 官网中对 key 属性的作用是怎样描述的。

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

只看上面的描述,你可能和我一样,还是不太理解什么意思。下面我会通过几个示例给大家介绍下 key 属性的作用有哪些。

v-for 中的 key

我们先看一个例子:

<template>
  <div>
    <button @click="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>
复制代码

上面的例子中,初始数组有三个元素,我们选中第一个元素的 checkbox 后,点击刷新按钮,数组在头部插入了一个新的元素,此时发生了奇怪的现象,为什么 checkbox 的状态保留在了新插入的元素上呢?不应该是跟随 name 为 11 的元素么?

这就是因为我们没有声明 key 属性造成的。下面我们根据 Vue VNode 更新原理,分析一下产生这样结果的原因:

在进行 VNode 虚拟 DOM 更新时,上述组件的新旧 VNode 结构大致是这样的:

// 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'
                        },
      		]
            }
      ]
    }
  ]
}
复制代码
// 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'
                        },
      		]
            }
      ]
    }
  ]
}
复制代码

当更新 ul元素的子节点时,Vue 内部执行 updateChildren 函数,比较同层级的所有子节点,采用的双端比较算法。

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)
    }
  }
复制代码

其中判断两个节点是否相同的 sameVnode 函数逻辑如下:

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)
      )
    )
  )
}
复制代码

由此可以看出,当未声明 key 属性时,新插入的 00 节点会认为与原 11 节点相同,更新过程如下:

其中共执行了3次 patchVnode过程和一次创建节点过程。在每一次的 patchVnode 中,都执行了文本节点 text 更新。由于 checkbox 控件没有修改,所以不进行更新,就地复用之前的内容,所以导致之前节点的状态被保留。

当我们声明了 key 属性后,它的执行逻辑如下:

第一次进行新旧 vnode 的头节点相等判断时,返回结果为 false,所以从尾部节点开始对比。上述过程也共执行了3次 patchVnode过程和一次创建节点过程。不过在每一次的 patchVnode 中,都没有执行任何 DOM 操作,因为标签内容没有修改。新插入的节点也是按照对应的顺序添加到 DOM 中的。不同于未声明 key 时,实际新创建的节点并不是我们以为的新元素 00,而是 33。

所以,在 v-for 中使用 key,能够提高组件的渲染速度,并且让组件按照预期重新排序,不会就地复用。(注意:使用 index 作为 key 和不使用 key 效果是一样的,所以不要使用 index 作为 key 值)

v-if 使用 key 管理可复用元素

这里举一个官网提供的示例,如果你允许用户在不同的登录方式之间切换:

<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>
复制代码

那么在上面的代码中切换 loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input> 不会被替换掉——仅仅是替换了它的 placeholder。

这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key attribute 即可:

<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>
复制代码

现在,每次切换时,输入框都将被重新渲染。

注意,<label> 元素仍然会被高效地复用,因为它们没有添加 key attribute

相同标签元素切换触发过渡效果

<template>
  <transition mode="out-in">
    <button v-if="show" @click="show = false">显示</button>
    <button v-else @click="show = true">隐藏</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>

复制代码

当未使用key属性时,交互效果如下:

test1.gif

此时并未触发过渡效果,是因为按钮切换时,会复用之前的按钮,只是更新了按钮的文本,所以没有涉及元素的新增和删除,导致过渡效果没有触发。

添加 key 属性之后:

<template>
  <transition mode="out-in">
    <button v-if="show" @click="show = false" key="show">显示</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>

复制代码

test2.gif

此时动画正常执行。

总结

  • 在 v-for 中使用 key,能够提高组件的渲染速度,并且让组件按照预期重新排序,不会就地复用。(注意:使用 index 作为 key 和不使用 key 效果是一样的,所以不要使用 index 作为 key 值)
  • 在 v-if 中可以使用 key 管理可复用的元素。
  • 使用 key 属性,可以解决相同标签元素切换时过渡效果未触发问题。

Supongo que te gusta

Origin juejin.im/post/7066302277153685512
Recomendado
Clasificación