vue组件$listeners和$attrs在透传时和新增事件和属性的合并规则

2,718 阅读2分钟

背景

在某些相对复杂的项目中,我们抽取的公共组件可能存在深层嵌套的场景,例如:

graph LR
App.vue --> Outer.vue --> Inner.vue

我们会在App.vue里带上业务中提供的数据,一层层的传递进去,在传递的过程中一般有两种方案:

  1. 通过props具名接收,再继续向内层传递
  2. 通过$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>

image.png

我们发现同名事件类似于数组合并做的contact,都会执行。属性是进行的覆盖。

属性覆盖

属性有覆盖,那么是谁覆盖谁呢,是根据先后顺序吗?我们来给 Outer.vue的代码做下调整,把v-bind放到后面

<template>
  <InnerComp v-on="$listeners" :attr-a="innerA" @event-a="eventA" v-bind="$attrs"></InnerComp>
</template>

image.png

我们发现输出结果没有变化,说明不管属性定位置在前还是在后,具名属性会都会覆盖v-bind里绑定的同名属性

如何覆盖事件,而不做合并

由于同名事件会合并在一起,都会被执行,单有的场景我们是想在中间层组件对上层的事件做覆盖,这时候怎么办呢? 答案是:扩展运算符。我们再对Outer.vue做一下改写。

// Outer.vue
<template>
  <InnerComp v-on="{...$listeners, 'event-a':eventA}" :attr-a="innerA" v-bind="$attrs"></InnerComp>
</template>

image.png 此时仅执行了一次event-a事件,大功告成。

PS:$listener$attrs,我们推荐使用扩展运算符的方式来做透传,这样可以避免一些异常问题。