Vue.js 2中如何让$emit事件仅作用于触发它的组件
问题修复:多实例Vue多选组件全选/清空互不影响
问题根源
- 全局DOM选择器跨组件干扰:
document.getElementsByName('option')会获取页面上所有同名复选框,点击全选/清空时会操作所有组件的复选框。 - 违反Vue单向数据流规则:子组件直接修改传入的
selectedProp(调用add()/clear()),可能导致状态混乱。 - 未用响应式绑定控制视图:复选框的
checked状态未与selected集合绑定,依赖手动DOM操作无法实现组件内状态同步。
修复步骤
1. 用响应式绑定替代DOM操作控制复选框
在模板中给复选框绑定checked属性,通过判断选项是否在selected集合中实现状态同步:
<input type="checkbox" :checked="isOptionSelected(option)" @change="(e) => { onChange(e.target.checked, option) }" />
添加辅助方法判断选中状态:
methods: { isOptionSelected(option) { const value = this.optionValueField ? option[this.optionValueField] : option; return this.selected.has(value); }, // ...其他方法 }
2. 修改全选/清空逻辑,仅操作当前组件状态
移除全局DOM选择代码,通过创建新的Set实例并触发input事件通知父组件更新状态,依赖响应式绑定自动刷新复选框:
selectAll() { const newSelected = new Set(this.selected); this.options.forEach(option => { const value = this.optionValueField ? option[this.optionValueField] : option; newSelected.add(value); }); this.$emit('input', newSelected); }, clear() { this.$emit('input', new Set()); }
3. 修正Prop修改方式,遵循单向数据流
子组件不再直接修改this.selected,而是创建新Set实例并通过$emit传递给父组件,由父组件更新状态:
onChange(checked, option) { const newSelected = new Set(this.selected); const value = this.optionValueField ? option[this.optionValueField] : option; if (checked) { newSelected.add(value); } else { newSelected.delete(value); } this.$emit('input', newSelected); }
4. 可选:为复选框添加唯一标识
给每个组件的复选框添加基于组件_uid的唯一name,彻底避免DOM选择器冲突:
<input type="checkbox" :name="`option-${_uid}`" :checked="isOptionSelected(option)" @change="(e) => { onChange(e.target.checked, option) }" />
完整修复后的组件代码
Vue.component('v-multiselect', { template: ` <b-field :label="label"> <template slot="label"> <span class="is-flex is-justify-content-space-between is-align-items-center"> <span> {{ label }} <span v-if="required" class="has-text-danger">*</span> </span> <span class="has-text-weight-light is-family-secondary is-size-6" > <span @click="selectAll" :disabled="selected.size === options.length" class="is-clickable has-text-primary">Select All</span> | <span @click="clear" :disabled="selected.size === 0" class="is-clickable has-text-primary">Clear</span> </span> </span> </template> <div class="panel"> <label v-for="option in options" class="panel-block"> <input type="checkbox" :name="`option-${_uid}`" :checked="isOptionSelected(option)" @change="(e) => { onChange(e.target.checked, option) }" /> <span class="ml-2">{{ optionLabelField ? option[optionLabelField] : option }}</span> </label> </div> </b-field> `, emits: ['input'], methods: { attachCustomCss() { const css_text = `input[type=checkbox] { -ms-transform: scale(1.5); /* IE */ -moz-transform: scale(1.5); /* FF */ -webkit-transform: scale(1.5); /* Safari and Chrome */ -o-transform: scale(1.5); /* Opera */ transform: scale(1.5); padding: 10px; } .panel { box-shadow: none !important; border: 1px solid #ededed; min-height: 40px; max-height: 158px; overflow-y: scroll; }` const css = document.createElement('style'); css.type = 'text/css'; css.setAttributeNode(document.createAttribute('scoped')); css.appendChild(document.createTextNode(css_text)); this.$el.appendChild(css); }, isOptionSelected(option) { const value = this.optionValueField ? option[this.optionValueField] : option; return this.selected.has(value); }, onChange(checked, option) { const newSelected = new Set(this.selected); const value = this.optionValueField ? option[this.optionValueField] : option; if (checked) { newSelected.add(value); } else { newSelected.delete(value); } this.$emit('input', newSelected); }, selectAll() { const newSelected = new Set(this.selected); this.options.forEach(option => { const value = this.optionValueField ? option[this.optionValueField] : option; newSelected.add(value); }); this.$emit('input', newSelected); }, clear() { this.$emit('input', new Set()); } }, props: { options: { type: Array, default: [] }, selected: { type: Set, default: () => new Set() }, label: { type: String, default: null }, required: { type: Boolean, default: false }, optionLabelField: { type: String, default: null }, optionValueField: { type: String, default: null } }, mounted() { this.attachCustomCss(); }, })
父组件使用示例
父组件需维护每个实例的独立selected状态:
<v-multiselect label="第一个多选组件" :options="options1" :selected="selected1" @input="selected1 = $event" ></v-multiselect> <v-multiselect label="第二个多选组件" :options="options2" :selected="selected2" @input="selected2 = $event" ></v-multiselect>
new Vue({ el: '#app', data() { return { options1: ['选项1', '选项2', '选项3'], selected1: new Set(), options2: ['A选项', 'B选项', 'C选项'], selected2: new Set() } } })
内容的提问来源于stack exchange,提问作者ktxdev
相关产品推荐
相关产品推荐

