$attrs和$listeners说明

799 阅读3分钟

$attrs

$attrs属性:包含父组件传过来的且子组件未使用props属性和emits事件声明接收的数据,包括class,style,v-bind绑定的属性、v-on绑定的监听器等。当一个组件没有声明任何prop时,$attrs会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件,这在创建高阶的组件时会非常有用。

比如对于第三方库组件的二次封装,如果往二次封装的组件传入一些属性,并且想要将这些属性传给第三方库组件,最简单的方式就是在组件中去一个个定义 props,然后再传给第三方库组件,但是这种方法非常麻烦,毕竟第三方库组件就有很多属性Attributes,这个时候可以使用 $attrs(属性透传)去解决这个问题。

继承第三方组件的 Attributes 属性,大概意思就是调用的组件传递的属性直接传递到第三方库组件中,数据流的方向是这样的:前端调用传递属性数据->已经二次封装的组件的属性数据->第三方原生组件属性数据。

代码演示

调用二次封装传递属性数据的父组件src/home/index.vue

<script lang="ts" setup>
import MyButton from "../components/myButton.vue";
import { ElButton } from "element-plus";
const handleClick = (e,val) => {
  console.log(e,77,val)
}
</script>

<template>
  <div>
    <MyButton :round='true' type='warning' name='按钮1' msg='我是消息1' style="color: blue;" class="mybutton" @click="handleClick($event,1)" ></MyButton>
    <MyButton  type='danger' name='按钮2' msg='我是消息2' style="color: white;" class="mybutton" @click="handleClick($event,2)" ></MyButton>
  </div>
</template>

<style scoped>
.mybutton {
  height: 50px;
  width: 100px;
  text-align: center;
  line-height: 50px;
}
</style>

二次封装的子组件src/components/myButton.vue

<script setup lang="ts">
import { ElButton } from "element-plus";
import { useAttrs } from "vue";
const attrs = useAttrs()
console.log(attrs,99)
const props = defineProps({
    name:{
        type:String,
        default:""
    }
})
</script>

<template>
  <div>
     <el-button v-bind="$attrs">{{props.name}}</el-button>
  </div>
</template>

除了myButton这个组件本身定义的 props属性中的name字段,其余home父组件绑定的所有其他属性都在$attrs属性中,如下图:

image.png

如下图:默认情况下,父组件传递的所有属性但没有被子组件解析为props属性的会被透。当有一个根元素的子组件时,这些绑定会被作为一个常规的HTML属性应用在子组件的根节点元素上。

image.png

上面的案例中,是存在问题的,传给子组件的click事件可能会触发多次。因为 $attrs 包含了父组件传递下来的事件监听器,这些监听器在子组件中会被重复绑定,事件默认是在冒泡阶段触发,所以绑定在子元素的事件就会冒泡到父元素导致父元素同一类型的事件被触发,这样事件就会触发多次。

调用二次封装传递属性数据的父组件src/home/index.vue

<script lang="ts" setup>
import MyButton from "../components/myButton.vue";
import { ElButton } from "element-plus";
const handleClick = (e, val) => {
  console.log(e, 77, val);
};
</script>

<template>
  <div>
    <MyButton
      :round="true"
      type="warning"
      name="按钮1"
      msg="我是消息1"
      style="color: blue"
      class="mybutton"
      @click="handleClick($event, 1)"
    ></MyButton>
    <MyButton
      type="danger"
      name="按钮2"
      msg="我是消息2"
      style="color: white"
      class="mybutton"
      @click.stop="handleClick($event, 2)"
    ></MyButton>
  </div>
</template>

<style scoped>
.mybutton {
  height: 50px;
  width: 100px;
  text-align: center;
  line-height: 50px;
  background-color: #30f009;
  margin-bottom: 10px;
}
</style>

二次封装的子组件src/components/myButton.vue

<script setup lang="ts">
import { ElButton } from "element-plus";
import { useAttrs } from "vue";
const attrs = useAttrs()
console.log(attrs,99)
const props = defineProps({
    name:{
        type:String,
        default:""
    }
})
</script>

<template>
  <div>
     <el-button v-bind="$attrs">{{props.name}}</el-button>
  </div>
</template>

下面两张图:可以得出监听器在子组件中被重复绑定(根元素<div></div><el-button></<el-button>都绑定了监听器)

image.png

image.png

下图可以看出,点击子组件的根元素事件触发了一次,因为事件触发源就是子组件的根元素,捕获和冒泡都是同一个点。点击子元素的时候事件触发了两次,就是事件触发源是子元素,在冒泡阶段触发父元素也就是子组件的根元素的事件。

8.gif

下图可以看出:对于按钮2@click.stop="handleClick($event, 2)"阻止了事件冒泡,事件就只触发了一次。

9.gif

上述问题避免 $attrs 导致的事件重复绑定问题解决方法:

  1. 明确绑定事件:确保在子组件中明确地绑定事件,而不是通过 $attrs 自动绑定所有属性。可以手动监听click 事件并使用 emit('click', event) 显式地触发事件,这样就完全控制了事件的传递过程。这种方式避免了 $attrs 自动绑定带来的重复事件绑定问题。

调用二次封装传递属性数据的父组件src/home/index.vue

<template>
  <div>
    <MyButton
      :round="true"
      type="warning"
      name="按钮1"
      msg="我是消息1"
      style="color: blue"
      class="mybutton"
      @click="handleClick($event, 1)"
    ></MyButton>
    <MyButton
      type="danger"
      name="按钮2"
      msg="我是消息2"
      style="color: white"
      class="mybutton"
      @click="handleClick($event, 2)"
    ></MyButton>
  </div>
</template>

<style scoped>
.mybutton {
  height: 50px;
  width: 100px;
  text-align: center;
  line-height: 50px;
  background-color: #30f009;
  margin-bottom: 10px;
}
</style>

二次封装的子组件src/components/myButton.vue

<script setup lang="ts">
import { ElButton } from "element-plus";
import { useAttrs } from "vue";
const attrs = useAttrs()
console.log(attrs,99)
const props = defineProps({
    name:{
        type:String,
        default:""
    }
})
const emit = defineEmits(['click']);
const clickHancdle = () => {
  emit('click','事件触发')
}
</script>

<template>
  <div>
     <el-button v-bind="$attrs" @click="clickHancdle">{{props.name}}</el-button>
  </div>
</template>

下图可以看出父组件传过来的监听器没有被$attrs接受了

image.png

下图可以看出监听器在子组件中没有被重复绑定,根元素<div></div>没有绑定监听器,而是<el-button></<el-button>绑定了监听器

image.png

image.png

但此时子组件emit触发的事件,父组件中事件接受的事件对象就是子组件传递的数据

10.gif

  1. 通过设置 inheritAttrs: false,可以防止 Vue 自动将父组件的属性绑定到子组件的根元素上,然后可以在子组件中显式地绑定需要的属性和事件。 设置 inheritAttrsfalse 时,这些父组件传递的属性可以通过 $attrs 这个实例属性来访问,并且可以通过 v-bind 来显式绑定在一个非根节点的元素上。

调用二次封装传递属性数据的父组件src/home/index.vue

<script lang="ts" setup>
import MyButton from "../components/myButton.vue";
import { ElButton } from "element-plus";
const handleClick = (e, val) => {
  console.log(e, 77, val);
};
</script>

<template>
  <div>
    <MyButton
      :round="true"
      type="warning"
      name="按钮1"
      msg="我是消息1"
      style="color: blue"
      class="mybutton"
      @click="handleClick($event, 1)"
    ></MyButton>
    <MyButton
      type="danger"
      name="按钮2"
      msg="我是消息2"
      style="color: white"
      class="mybutton"
      @click="handleClick($event, 2)"
    ></MyButton>
  </div>
</template>

<style scoped>
.mybutton {
  height: 50px;
  width: 100px;
  text-align: center;
  line-height: 50px;
}
</style>

二次封装的子组件src/components/myButton.vue

<script setup lang="ts">
import { ElButton } from "element-plus";
import { useAttrs } from "vue";
const attrs = useAttrs()
console.log(attrs,99)
const props = defineProps({
    name:{
        type:String,
        default:""
    }
})
defineOptions({
  name:'myButton',
  inheritAttrs:false
})
</script>

<template>
  <div>
    <el-button v-bind="$attrs">{{props.name}}</el-button>
  </div>
</template>

image.png

监听器在子组件中通过 v-bind显式绑定在一个非根节点的元素上,没有重复绑定

image.png

11.gif

但是父组件传入的class属性并没有生效,样式没有起作用,这是父组件的style标签加上scoped属性,css样式作用域只能作用于子组件的根组件,如果此时需要父组件控制子组件根元素的后代元素需要样式穿透。

defineOptions是一个宏,是在Vue3.3+中新增的新特性(宏就是无需通过import { defineOptions } from 'vue'这样导入,可以直接使用 )。

defineOptions配置项是解决一个script标签以Options API的方式配置组件名和配置组件禁用透传。

defineOptions之前:

<!-- src/components/Com.vue -->
<script setup>
defineOptions({
  name: 'ComponentName',
})
</script>

<script>
export default {
  inheritAttrs: false
}
</script>

<template>
  <div>Com Component</div>
</template>

使用defineOptions

<!-- src/components/Com.vue -->
<script setup>
defineOptions({
  name: 'ComponentName',
  inheritAttrs: false,
})
</script>

<template>
  <div>Com Component</div>
</template>

$listeners

$listeners:包含了父作用域中的(不含.sync修饰器的)v-on事件监听器,这些监听器可以在组件内部使用,也可以通过v-on="$listeners"传递给子组件使用。

跟上面的属性传递一样,如果往二次封装组件传入一些事件,并且想要将这些事件传给第三方原生组件,这里需要用到 $listeners

在 Vue3 中,取消了$listeners这个组件实例的属性,将其事件的监听都整合到了$attrs上,因此直接通过v-bind=$attrs属性就可以进行event事件的透传。

vue2:

调用二次封装传递属性数据的父组件

<MyInput @change="change" @focus="focus" @input="input"></MyInput>

二次封装的子组件

<template> 
    <div class="my-input"> 
        <el-input v-bind="$attrs" v-on="$listeners"></el-input> 
    </div> 
</template>
<script>
export default {
  name: 'myInput',
  mounted() {
    console.log('this.$attrs', this.$attrs)
    console.log('this.$listeners', this.$listeners)
  }
}
</script>