Vue v-for循环单元素切换class 子组件props更新不响应问题
问题背景
需要在v-for渲染的时段列表中实现单选交互效果:
- 同一时间仅允许单个列表项持有选中状态class
- 点击某一项时,移除上一选中项的class,为当前点击项添加选中class
现有代码出现异常:通过Vue DevTools可观测到timeslot.selected值已按逻辑正确更新,但子组件的:class绑定完全不响应prop变化;后续尝试将prop值同步到子组件本地data、添加带immediate/deep配置的watch监听prop,依然无法触发更新——watch逻辑完全不执行。
问题代码
AvailableTimeslots.vue(父组件)
<template> <div> <timeslot v-for="timeslot in timeslots" :key="timeslot.id" :timeslot="timeslot" @pickedTimeslot="pickTime(timeslot)" ></timeslot> </div> </template> <script> import Timeslot from './Timeslot.vue'; export default { name: 'AvailableTimeslots', components: { Timeslot }, data() { return { timeslots: [] }; }, mounted() { this.getTimeslots(); }, methods: { pickTime(timeslot) { this.deselectTimeslots(timeslot.id); }, async getTimeslots() { // 接口拉取时段列表逻辑(省略) this.deselectAllTimeslots(); }, deselectAllTimeslots() { this.timeslots.forEach(timeslot => { timeslot.selected = false; }); }, deselectTimeslots(id) { this.timeslots.forEach(timeslot => { if (timeslot.id !== id) timeslot.selected = false; else timeslot.selected = true; }); }, } } </script>
Timeslot.vue(子组件初始版本)
<template> <div class="card" :class="{ 'selected-timeslot': this.timeslot.selected }" @click="pickTime()" > {{ timeslot.from }} </div> </template> <script> export default { name: 'Timeslot', props: [ 'timeslot' ], methods: { pickTime() { this.$emit('pickedTimeslot', this.timeslot); }, }, } </script>
Timeslot.vue(添加watch的尝试版本)
<template> <div class="card" :class="{ 'selected-timeslot': this.isSelected }" @click="pickTime()" > {{ timeslot.from }} </div> </template> <script> export default { name: 'Timeslot', props: [ 'timeslot' ], data() { return { isSelected: this.timeslot.selected, }; }, methods: { pickTime() { this.$emit('pickedTimeslot', this.timeslot); }, }, watch: { timeslot: { handler() { this.$nextTick(() => { this.isSelected = this.timeslot.selected; }); }, immediate: true, deep: true, }, }, } </script>
问题根因
子组件无法响应prop更新的核心原因是Vue 2的响应式系统限制:
- Vue 2无法检测对象上新增的属性。接口拉取到timeslots数据后,代码直接通过
timeslot.selected = false给每个列表项新增selected属性,这个属性没有被Vue注入响应式的getter/setter,后续修改该属性值时,Vue不会派发更新通知,自然不会触发视图重渲染、子组件prop更新、deep监听回调。 - Vue DevTools能观测到值变化,是因为DevTools直接读取了对象的属性值,不依赖Vue的响应式更新通知。
如果要沿用原有给每个项加selected属性的写法,新增属性时必须用this.$set(timeslot, 'selected', false)做响应式声明,修改值时才能触发更新,但这种方案需要遍历全量列表更新状态,并不是最优解。
更简洁的单选实现方案
不需要给每个列表项新增selected属性,直接在父组件维护当前选中项的唯一标识即可,逻辑更轻量,也不会踩响应式坑:
父组件修改
<template> <div> <timeslot v-for="timeslot in timeslots" :key="timeslot.id" :timeslot="timeslot" :is-selected="timeslot.id === selectedId" @pickedTimeslot="selectedId = timeslot.id" ></timeslot> </div> </template> <script> import Timeslot from './Timeslot.vue'; export default { name: 'AvailableTimeslots', components: { Timeslot }, data() { return { timeslots: [], selectedId: null // 只存当前选中项的id,无需遍历修改全量数据 }; }, mounted() { this.getTimeslots(); }, methods: { async getTimeslots() { // 接口拉取时段列表逻辑(省略) this.selectedId = null; // 拉取新数据时清空选中状态即可 } } } </script>
子组件修改
<template> <div class="card" :class="{ 'selected-timeslot': isSelected }" @click="pickTime()" > {{ timeslot.from }} </div> </template> <script> export default { name: 'Timeslot', // 明确声明prop类型,可读性更好 props: { timeslot: { type: Object, required: true }, isSelected: { type: Boolean, default: false } }, methods: { pickTime() { this.$emit('pickedTimeslot', this.timeslot); }, }, } </script>
该方案不需要遍历全量列表修改状态,不需要watch、不需要本地同步data,状态来源单一,完全符合Vue单向数据流的设计规范。
内容的提问来源于stack exchange,提问作者lindastralberg
相关产品推荐
相关产品推荐

