背景
在某些相对复杂的项目中,我们抽取的公共组件可能存在深层嵌套的场景,例如:
graph LR
App.vue --> Outer.vue --> Inner.vue
我们会在App.vue里带上业务中提供的数据,一层层的传递进去,在传递的过程中一般有两种方案:
- 通过
props
具名接收,再继续向内层传递 - 通过
$listeners
和$attrs
变量配合v-on和v-bind进行批量透传
方案1
优点:我们可以在组件中清晰的看到所拥有的props和events变量名。
缺点:属性较多的时候,写起来太繁琐,外层新增删除属性和事件时容易有遗漏。
方案2
优点:不需要关心上层传递了多少属性和事件,通通转发,维护方便。
缺点:内层组件不会书写具名props和events,需要通过顶层组件查看传递的key名,或者自己打印$listeners
和$attrs
问题
我们在使用 方案2 的时候,中间层组件有可能需要新增或者修改一些属性和事件,这时如果有同名属性和事件的时候,vue会去如何处理呢?针对这个问题我们来做下实验:
实验
// App.vue
<template>
<div id="app">
<OuterComp :attr-a="outerA" :attr-b="outerA" @event-a="eventA" @event-b="eventB" />
</div>
</template>
<script>
import OuterComp from './components/outer-comp.vue'
export default {
name: 'App',
components: {
OuterComp
},
data(){
return {
outerA:'outerA',
outerB:'outerB'
}
},
methods: {
eventA(){
console.log('外层 eventA');
},
eventB(){
console.log('外层 eventB');
}
}
}
</script>
// Outer.vue
<template>
<InnerComp v-bind="$attrs" v-on="$listeners" :attr-a="innerA" @event-a="eventA"></InnerComp>
</template>
<script>
import InnerComp from './inner-comp.vue'
export default {
components: {InnerComp},
data() {
return {
innerA:'innerA'
}
},
mounted(){},
methods: {
eventA(){
console.log('内层 eventA');
}
}
};
</script>
// Inner.vue
<template>
<div></div>
</template>
<script>
export default {
data() {
return {}
},
methods: {
},
mounted(){
console.log(this.$attrs['attr-a']);
this.$emit('event-a');
}
};
</script>
我们发现同名事件类似于数组合并做的contact,都会执行。属性是进行的覆盖。
属性覆盖
属性有覆盖,那么是谁覆盖谁呢,是根据先后顺序吗?我们来给 Outer.vue
的代码做下调整,把v-bind
放到后面
<template>
<InnerComp v-on="$listeners" :attr-a="innerA" @event-a="eventA" v-bind="$attrs"></InnerComp>
</template>
我们发现输出结果没有变化,说明不管属性定位置在前还是在后,具名属性会都会覆盖v-bind里绑定的同名属性。
如何覆盖事件,而不做合并
由于同名事件会合并在一起,都会被执行,单有的场景我们是想在中间层组件对上层的事件做覆盖,这时候怎么办呢?
答案是:扩展运算符
。我们再对Outer.vue
做一下改写。
// Outer.vue
<template>
<InnerComp v-on="{...$listeners, 'event-a':eventA}" :attr-a="innerA" v-bind="$attrs"></InnerComp>
</template>
此时仅执行了一次event-a
事件,大功告成。
PS:$listener
和$attrs
,我们推荐使用扩展运算符
的方式来做透传,这样可以避免一些异常问题。