本文代码均可直接复制到 Vue演练场 运行
什么是 key
key 是 Vue 提供的一种标签上的属性(attribute),使用方法和其他 attribute 一样。你可以使用 v-bind
指令给 key
绑定动态数据:
<template>
<div class="box" key="1"></div>
<div class="box" v-for="item in items" :key="item.id"></div>
</template>
key
的作用
-
复用元素:数据发生变化更新视图时,Vue会通过
key
来识别每个元素的身份,从而尽可能复用已有元素。 -
根据 key 的变化顺序来重新排列元素:数据发生变化更新视图时,Vue可以使用
key
来追踪每个节点的身份,确定哪些元素是相同的,可以移动而不是重新创建,这样可以提高列表渲染的性能。 -
销毁与创建:如果新的元素里缺少了某些key那么对应元素就会被销毁,新增的key就会创建对应元素。
如果没有key
,Vue会采用“就地更新”的策略。
在下面 各种情况 部分,可以看到就地更新的机制,以及使用 key
之后带来的性能提升
什么时候需要使用 key
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
简单来说,如果你的子元素都是根据 v-for="item in items"
里的 item
获取的结果进行渲染,即使你不使用 key
也不会出现问题。
各种情况
不使用 key
子组件没有自己的状态
<template>
<span>
子组件 {{ id }}
</span>
</template>
<script setup>
import { watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
id: Number
})
onMounted(() => {
console.log(`id为:${props.id}的子组件挂载完成`)
})
onUpdated(() => {
console.log(`id为:${props.id}的子组件更新完成`)
})
onUnmounted(() => {
console.log(props.id + '')
console.log(`id为:${props.id}的子组件已被销毁`)
})
</script>
<template>
<button @click="insertLast"> 尾部插入一条数据 </button>
<button @click="insertCenter"> 中间位置插入一条数据 </button>
<button @click="insertFirst"> 头部插入一条数据 </button>
<ul>
<li v-for="(item, index) in items">
{{ item.msg }}
<Comp :id="item.id" />
<button @click="del(index)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue';
const items = ref([
{
id: 1,
msg: '第一项',
},
{
id: 2,
msg: '第二项',
},
])
const insertLast = () => {
items.value.push({
id: 3,
msg:'第三项'
})
}
const insertCenter = () => {
items.value.splice(1,0,{
id: 4,
msg:'第四项'
})
}
const insertFirst = () => {
items.value.unshift({
id: 0,
msg:'第零项'
})
}
const del = (index) => {
items.value.splice(index, 1)
}
</script>
-
点击
尾部插入一条数据
,观察控制台输出结果:["id为:3的子组件挂载完成"]。发现创建了一个新组件。我们新增的数据id是3,没问题。 -
点击
头部插入一条数据
,观察控制台输出结果:["id为:0的子组件更新完成", "id为:1的子组件更新完成", "id为:2的子组件更新完成", "id为:3的子组件挂载完成"]。出现了问题,我们新增id为0的元素是被更新的,原来已经存在id为3的元素却被重新创建。 这就是 Vue 的 “就地更新”策略。更新逻辑为:
子组件有自己的状态
<template>
<span>
子组件 {{ id }}
<input v-model="name" />
</span>
</template>
<script setup>
import { ref, watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
id: Number
})
const name = ref('')
onMounted(() => {
console.log(`id为:${props.id}的子组件挂载完成,此时组件自己的输入框值为:${name.value}`)
})
onUpdated(() => {
console.log(`id为:${props.id}的子组件更新完成,此时组件自己的输入框值为:${name.value}`)
})
onUnmounted(() => {
console.log(`id为:${props.id}的子组件已被销毁`)
})
</script>
<template>
<button @click="insertLast"> 尾部插入一条数据 </button>
<button @click="insertCenter"> 中间位置插入一条数据 </button>
<button @click="insertFirst"> 头部插入一条数据 </button>
<ul>
<li v-for="(item, index) in items">
{{ item.msg }}
<Comp :id="item.id" />
<button @click="del(index)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue';
const items = ref([
{
id: 1,
msg: '第一项',
},
{
id: 2,
msg: '第二项',
},
])
const insertLast = () => {
items.value.push({
id: 3,
msg:'第三项'
})
}
const insertCenter = () => {
items.value.splice(1,0,{
id: 4,
msg:'第四项'
})
}
const insertFirst = () => {
items.value.unshift({
id: 0,
msg:'第零项'
})
}
const del = (index) => {
items.value.splice(index, 1)
}
</script>
列表渲染输出结果现在依赖子组件自己的状态了,就地更新策略出现了问题。
- 首先就地更新,原来元素顺序没有改变,值为1的输入框现在仍是处于首位;
- 头部添加数据响应式数据发生变化,需要更新内容,第一项id变为0、msg变为第零项,但是input是子组件自己维护状态的,不受父组件数据变化的影响;
- 因此,页面就出现了值为1的输入框,出现在第零项的后面。
使用 index
索引作为 key
子组件没有自己的状态
<template>
<span>
子组件 {{ id }}
</span>
</template>
<script setup>
import { watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
id: Number
})
onMounted(() => {
console.log(`id为:${props.id}的子组件挂载完成`)
})
onUpdated(() => {
console.log(`id为:${props.id}的子组件更新完成`)
})
onUnmounted(() => {
console.log(`id为:${props.id}的子组件已被销毁`)
})
</script>
<template>
<button @click="insertLast"> 尾部插入一条数据 </button>
<button @click="insertCenter"> 中间位置插入一条数据 </button>
<button @click="insertFirst"> 头部插入一条数据 </button>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item.msg }}
<Comp :id="item.id" />
<button @click="del(index)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue';
const items = ref([
{
id: 1,
msg: '第一项',
},
{
id: 2,
msg: '第二项',
},
])
const insertLast = () => {
items.value.push({
id: 3,
msg:'第三项'
})
}
const insertCenter = () => {
items.value.splice(1,0,{
id: 4,
msg:'第四项'
})
}
const insertFirst = () => {
items.value.unshift({
id: 0,
msg:'第零项'
})
}
const del = (index) => {
items.value.splice(index, 1)
}
</script>
-
点击
尾部插入一条数据
,观察控制台输出结果:["id为:3的子组件挂载完成"]。发现创建了一个新组件。我们新增的数据id是3,没问题。 -
点击
头部插入一条数据
,观察控制台输出结果:["id为:0的子组件更新完成", "id为:1的子组件更新完成", "id为:2的子组件更新完成", "id为:3的子组件挂载完成"]。
看起来好像效果和不添加 key
一样,但其实还是比较了 key
的。
子组件有自己的状态
<template>
<span>
子组件 {{ id }}
<input v-model="name" />
</span>
</template>
<script setup>
import { ref, watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
id: Number
})
const name = ref('')
onMounted(() => {
console.log(`id为:${props.id}的子组件挂载完成,此时组件自己的输入框值为:${name.value}`)
})
onUpdated(() => {
console.log(`id为:${props.id}的子组件更新完成,此时组件自己的输入框值为:${name.value}`)
})
onUnmounted(() => {
console.log(`id为:${props.id}的子组件已被销毁`)
})
</script>
<template>
<button @click="insertLast"> 尾部插入一条数据 </button>
<button @click="insertCenter"> 中间位置插入一条数据 </button>
<button @click="insertFirst"> 头部插入一条数据 </button>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item.msg }}
<Comp :id="item.id" />
<button @click="del(index)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue';
const items = ref([
{
id: 1,
msg: '第一项',
},
{
id: 2,
msg: '第二项',
},
])
const insertLast = () => {
items.value.push({
id: 3,
msg:'第三项'
})
}
const insertCenter = () => {
items.value.splice(1,0,{
id: 4,
msg:'第四项'
})
}
const insertFirst = () => {
items.value.unshift({
id: 0,
msg:'第零项'
})
}
const del = (index) => {
items.value.splice(index, 1)
}
</script>
出现了和不添加 key
并且子组件有自己的状态同样的问题,属于第一项的输入框,更新之后出现在第零项。
涉及到子组件有自己状态或者表单输入时,不仅仅是头部插入,只要元素顺序变化(删除其中某个元素,随机插入,交换顺序等),不管是不添加 key
还是使用 index
作为 key
渲染都会出现问题。
使用唯一 key
子组件没有自己的状态
<template>
<span>
子组件 {{ id }}
</span>
</template>
<script setup>
import { watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
id: Number
})
onMounted(() => {
console.log(`id为:${props.id}的子组件挂载完成`)
})
onUpdated(() => {
console.log(`id为:${props.id}的子组件更新完成`)
})
onUnmounted(() => {
console.log(`id为:${props.id}的子组件已被销毁`)
})
</script>
<template>
<button @click="insertLast"> 尾部插入一条数据 </button>
<button @click="insertCenter"> 中间位置插入一条数据 </button>
<button @click="insertFirst"> 头部插入一条数据 </button>
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ item.msg }}
<Comp :id="item.id" />
<button @click="del(index)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue';
const items = ref([
{
id: 1,
msg: '第一项',
},
{
id: 2,
msg: '第二项',
},
])
const insertLast = () => {
items.value.push({
id: 3,
msg:'第三项'
})
}
const insertCenter = () => {
items.value.splice(1,0,{
id: 4,
msg:'第四项'
})
}
const insertFirst = () => {
items.value.unshift({
id: 0,
msg:'第零项'
})
}
const del = (index) => {
items.value.splice(index, 1)
}
</script>
- 点击尾部插入元素控制台打印:["id为:3的组件挂载完成"]
- 再点击头部插入元素控制台打印:["id为:0的组件挂载完成"],比之前少了几个,key为1,id为1的元素更新之后仍是key为1,id为1,数据也没发生变化因此无需更新此元素。
子组件有自己的状态
<template>
<span>
子组件 {{ id }}
<input v-model="name" />
</span>
</template>
<script setup>
import { ref, watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
id: Number
})
const name = ref('')
onMounted(() => {
console.log(`id为:${props.id}的子组件挂载完成,此时组件自己的输入框值为:${name.value}`)
})
onUpdated(() => {
console.log(`id为:${props.id}的子组件更新完成,此时组件自己的输入框值为:${name.value}`)
})
onUnmounted(() => {
console.log(`id为:${props.id}的子组件已被销毁`)
})
</script>
<template>
<button @click="insertLast"> 尾部插入一条数据 </button>
<button @click="insertCenter"> 中间位置插入一条数据 </button>
<button @click="insertFirst"> 头部插入一条数据 </button>
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ item.msg }}
<Comp :id="item.id" />
<button @click="del(index)">删除</button>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue';
const items = ref([
{
id: 1,
msg: '第一项',
},
{
id: 2,
msg: '第二项',
},
])
const insertLast = () => {
items.value.push({
id: 3,
msg:'第三项'
})
}
const insertCenter = () => {
items.value.splice(1,0,{
id: 4,
msg:'第四项'
})
}
const insertFirst = () => {
items.value.unshift({
id: 0,
msg:'第零项'
})
}
const del = (index) => {
items.value.splice(index, 1)
}
</script>
同样可以正确渲染:
总结
在子组件没有自己的状态或者没有表单输入内容时,不使用 key
、使用 index
作为 key
、使用唯一 key
,最后渲染的效果都是一样的。
不过观察控制台打印,只有在使用唯一 key
时打印内容最少,证明进行的更新最少,因此性能也就最好。
并且在子组件维护自己的状态或者子元素存在表单项时,也只有使用唯一 key
可以做到正确渲染。
其他用途
配合 Transition
实现动画
<!-- 实现一些动画效果 -->
<template>
<p style="overflow: hidden">
js框架:
<Transition name="text" mode="out-in">
<span :key="computedText" :style="computedStyle"> {{ computedText }} </span>
</Transition>
</p>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
const textList = [
'Vue',
'React',
'Angular'
]
const colorList = [
'rgb(66, 184, 131)',
'rgb(8, 126, 164)',
'rgb(239, 8, 19)'
]
const curIndex = ref(0)
const computedText = computed(() => textList[curIndex.value])
const computedStyle = computed(() => ({
color: colorList[curIndex.value],
display: 'inline-block'
}))
let timer = null
const autoChange = () => {
if (curIndex.value === textList.length - 1) {
curIndex.value = 0
} else {
curIndex.value += 1
}
}
onMounted(() => {
timer = setInterval(autoChange, 2000)
})
onBeforeUnmount(() => {
clearInterval(timer)
})
</script>
<style scoped>
.text-enter-active,
.text-leave-active {
transition: all .2s ease;
}
.text-enter-from {
transform: translateY(100%);
}
.text-leave-to {
transform: translateY(-100%);
}
</style>
主动重新渲染子组件
<!-- Comp.vue -->
<template>
<p>
子组件
</p>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('子组件挂载完成')
})
onUnmounted(() => {
console.log('子组件已被销毁')
})
</script>
<!-- App.vue -->
<template>
<Comp :key="count" />
<button @click="reRender">
重新渲染子组件
</button>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Comp from './Comp.vue'
const count = ref(0)
const reRender = () => {
count.value += 1
console.log(`子组件已被重新渲染 ${count.value} 次`)
// 点击按钮后控制台打印:
// 子组件已被重新渲染 1 次
// 子组件已被销毁
// 子组件挂载完成
}
</script>
使用 key
的注意事项
- 同一父元素下面子元素的
key
必须唯一 key
最好是和数据绑定的值,比如该条数据在数据库中的id
key
的数据类型只支持:string
|number
|symbol
- 哪个标签使用了
v-for
哪个标签添加key
<!-- 即使 template 不会被渲染成实际的DOM,也要在 template 上添加key,因为 v-for 在 template 上使用 -->
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>