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属性时,交互效果如下:
此时并未触发过渡效果,是因为按钮切换时,会复用之前的按钮,只是更新了按钮的文本,所以没有涉及元素的新增和删除,导致过渡效果没有触发。
添加 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>
此时动画正常执行。
总结
key 可以理解为组件的一个唯一标识,当进行 vnode 对比更新时,判断两个节点是否相同的第一个依据就是 key 值是否相同。
- 在 v-for 中使用 key,能够提高组件的渲染速度,并且让组件按照预期重新排序,不会就地复用。(注意:使用 index 作为 key 和不使用 key 效果是一样的,所以不要使用 index 作为 key 值)
- 在 v-if 中可以使用 key 管理可复用的元素。
- 使用 key 属性,可以解决相同标签元素切换时过渡效果未触发问题。
转载自:https://juejin.cn/post/7066302277153685512