在实际业务中,经常会有这样的场景:一堆消息卡片。比如:
对于这种情况,在Vue里,我们一般都会封装一个Card组件,然后在父组件中用v-for进行渲染。以下是一个随手写的例子(也可以在https://codesandbox.io/s/great-field-x95lw?fontsize=14&hidenavigation=1&theme=dark查看):
// App.vue
<template>
<div id="app">
<div v-for="(item, index) in arr"
:key="index">
<Card :title="item.name"
@remove="arr.splice(index, 1)"/>
</div>
</div>
</template>
<script>
export default {
name: 'App',
components: { Card },
data () {
return {
arr: [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
{ id: 3, name: 'baz' },
{ id: 4, name: 'buz' }
]
}
}
}
</script>
// Card.vue
<template>
<div v-if="showCard"
:style="{border: '1px solid red'}">
<span>{{title}} </span>
<button @click="removeCard">delete</button>
</div>
</template>
<script>
export default {
name: 'Card',
props: {
title: String
},
data () {
return {
showCard: true
}
},
methods: {
removeCard () {
this.$emit('remove')
setTimeout(() => {
this.showCard = false
// 此处展示删除动画
}, 500)
}
}
}
</script>
效果是这样的,看起来是磕碜了一点,但是和之前那个卡片比起来也就差个CSS,对吧:
太简单了!但是就这么一个简单的过程,但是里面却隐藏着一个bug:除了最后一张卡片,点击删除的时候,不仅仅会删除当前元素,还会删除其他的卡片。比如,我们点击删除第一张卡片:
第二个元素也没了。
这个问题当时困扰了我很久,索性去官方文档看看,果然找到了解释:
key
的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
再次回去查看逻辑,就会发现,这里因为采用了数组下标index作为key,这个key会根据数组中元素下标的变化而变化。点击删除后,父组件监听到Card的remove事件,就会从数组中移除对应index的元素,key随之变化,元素被重新排序,被删除元素的后一个元素的key就变成了被删除元素的key。此时0.5s的动画正在进行,等到动画结束后,showCard被置为false,但是此时的元素已经变成预定目标的后一个元素了(只认key不认内容),所以就会出现删除两个的情况。整个流程是这样的:
- 点击删除
- 触发remove事件
- 从数组中移除预定目标,key发生改变
- 视图重新渲染
- 动画结束,触发v-if,销毁目标的后一个元素
这个现象其实在很多别的语言里也会出现,比如Java的List.remove()
。