前言
vue通信手段有很多种,props/emit、vuex、event bus、provide/inject等。还有一种通信方式,那就是 $attrs 和 $listeners,之前早就听说这两个api,趁着有空来补补。这种方式挺优雅,使用起来也不赖。下面例子都会通过父、子、孙子,三者的关系来说明使用方式。
Vue 3 注意事项:
在 Vue 3 中,$listeners已被移除,其功能被合并到$attrs中。现在$attrs不仅包含属性,还包含事件监听器。

$attrs
官方解释:
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。
我的理解:
接收除了
props声明外的所有绑定属性(class、style除外)
图解:

由于child.vue 在 props 中声明了 name 属性,$attrs 中只有age、gender两个属性,输出结果为:
{ age: "20", gender: "man" }

上面打印结果为:
{height: '80', name: 'joe', gender: 'man'}//会把child页面的值,一起传过来
注:但若是child.vue页面有个prop:[“name”],则name不会传到grandson.vue页面中去,即:
{height: '80', gender: 'man'},如下:

另外可以在 grandson.vue 上通过 v-bind="$attrs",可以将属性继续向下传递,让 grandson.vue 也能访问到父组件的属性,这在传递多个属性时会显得很便捷,而不用一条条的进行绑定。
如果想要添加其他属性,可继续绑定属性。但要注意的是,继续绑定的属性和 $attrs 中的属性有重复时,继续绑定的属性优先级会更高。

打印结果为:
{height: '80', name: 'xth', gender: 'man'}//会把child页面的值,一起传过来,同时会以继续绑定的属性为最高优先级
$listeners
官方解释:
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
我的理解:
接收除了带有
.native事件修饰符的所有事件监听器
图解:

parent.vue 中对 child.vue 绑定了两个事件,带有.native的 click 事件和一个自定义事件,所以在 child.vue 中,输出 $listeners 的结果为:
{
customEvent: fn;
}

同 attrs 属性一样,可以通过 v-on="$listeners",将事件监听器继续向下传递,让 grandson.vue 访问到事件,且可以使用 $emit 触发 parent.vue 的函数。
如果想要添加其他事件监听器,可继续绑定事件。但要注意的是,继续绑定的事件和 $listeners 中的事件有重复时,不会被覆盖。当 grandson.vue 触发 customEvent 时,child.vue 和 parent.vue 的事件都会被触发,触发顺序类似于冒泡,先到 child.vue 再到 parent.vue。
以上转自:Vue - 组件通信之$attrs、$listeners
代码示例
1.$attrs 示例
<div id="app">
根组件
<hr />
<father :msg="msg" :rootName="name" :rootDetail="infor"></father>
</div>
<template id="father">
<div>
父组件---{{msg}}
<hr />
<son v-on="$listeners" v-bind="$attrs"></son>
</div>
</template>
<template id="son">
<div>子组件</div>
</template>
<script>
// 子组件
let son = {
template: "#son",
created() {
console.log(this.$attrs, "子组件,接受root的非prop的数据");
},
};
// 父组件
let father = {
template: "#father",
components: {
son,
},
props: ["msg"],
created() {
console.log(this.$attrs, "父组件,接受root的非prop的数据");
},
};
let vm = new Vue({
el: "#app",
components: {
father,
},
data() {
return {
msg: "根结点的数据",
name: "root",
infor: {
name: "xth",
age: 18,
address: "app根部",
},
};
},
});
</script>

2.$listeners 示例
<div id="app">
根组件
<hr />
<father @cli1="click1" @cli2="click2" @cli3.native="click3"></father>
</div>
<template id="father">
<div v-on="$listeners">
父组件 <button @click="$listeners.cli1">获取root方法cli1</button><br />
<hr />
<son v-on="$listeners"></son>
</div>
</template>
<template id="son">
<div>
子组件
<button @click="$listeners.cli2">获取root方法cli2</button>
</div>
</template>
<script>
// 子组件
let son = {
template: "#son",
created() {
console.log(this.$listeners, "子组件"); // 包含父级所有绑定的方法
},
};
// 父组件
let father = {
template: "#father",
components: {
son,
},
created() {
console.log(this.$listeners, "父组件,接受父级所有绑定的方法"); // 包含父级所有绑定的方法
},
};
let vm = new Vue({
el: "#app",
components: {
father,
},
methods: {
click1() {
console.log("点击事件cli1");
},
click2() {
console.log("点击事件cli2");
},
},
});
</script>

3.$attrs 、$listeners 、 prop 、 $emit 示例
<div id="app">
根组件
<hr />
<father
:msg="msg"
:rootName="name"
:rootDetail="infor"
@cli1="click1"
@cli2="click2"
@cli3.native="click3"
></father>
</div>
<template id="father">
<div v-on="$listeners">
父组件---{{msg}} <button @click="$listeners.cli1">获取root方法cli1</button><br />
<button @click="getRootMethod">获取root方法</button>
<hr />
<son v-on="$listeners" v-bind="$attrs" @fathermethod="fatherMethod"></son>
</div>
</template>
<template id="son">
<div>
子组件
<button @click="$listeners.cli2">获取root方法cli2</button>
<button @click="getFatherMethod">获取father方法</button>
</div>
</template>
<script>
// 子组件
let son = {
template: "#son",
created() {
console.log(this.$attrs, "子组件,接受root的非prop的数据");
console.log(this.$listeners, "子组件"); // 包含父级所有绑定的方法
},
methods: {
getFatherMethod() {
this.$emit("fathermethod");
},
},
};
// 父组件
let father = {
template: "#father",
components: {
son,
},
props: ["msg"],
created() {
console.log(this.$attrs, "父组件,接受root的非prop的数据");
console.log(this.$listeners, "父组件,接受父级所有绑定的方法"); // 包含父级所有绑定的方法
},
methods: {
getRootMethod() {
this.$parent.rootMethod();
},
fatherMethod() {
console.log("子组件的getFatherMethod方法点了我");
},
},
};
let vm = new Vue({
el: "#app",
components: {
father,
},
data() {
return {
msg: "根结点的数据",
name: "root",
infor: {
name: "xth",
age: 18,
address: "app根部",
},
};
},
methods: {
click1() {
console.log("点击事件cli1");
},
click2() {
console.log("点击事件cli2");
},
click3() {
console.log("点击事件cli3");
},
rootMethod() {
console.log("父组件的getRootMethod方法点了我");
},
},
});
</script>
Vue 3 中的使用方式
在 Vue 3 中,$listeners 已被移除,所有事件监听器现在都包含在 $attrs 中。以下是 Vue 3 的示例:
Vue 3 示例
<!-- App.vue (根组件) -->
<template>
<div>
根组件
<hr />
<Father :msg="msg" :rootName="name" :rootDetail="infor" @cli1="click1" @cli2="click2" />
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import Father from "./Father.vue";
const msg = ref("根结点的数据");
const name = ref("root");
const infor = reactive({
name: "xth",
age: 18,
address: "app根部",
});
const click1 = () => {
console.log("点击事件cli1");
};
const click2 = () => {
console.log("点击事件cli2");
};
</script>
<!-- Father.vue (父组件) -->
<template>
<div>
父组件---{{ msg }} <button @click="$attrs.onCli1">获取root方法cli1</button><br />
<hr />
<Son v-bind="$attrs" @fathermethod="fatherMethod" />
</div>
</template>
<script setup>
import { defineProps } from "vue";
import Son from "./Son.vue";
const props = defineProps(["msg"]);
const fatherMethod = () => {
console.log("子组件的getFatherMethod方法点了我");
};
</script>
<!-- Son.vue (子组件) -->
<template>
<div>
子组件
<button @click="$attrs.onCli2">获取root方法cli2</button>
<button @click="getFatherMethod">获取father方法</button>
</div>
</template>
<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["fathermethod"]);
const getFatherMethod = () => {
emit("fathermethod");
};
</script>
Vue 3 中的变化
$listeners被移除:所有事件监听器现在都包含在$attrs中- 事件访问方式:在 Vue 3 中,事件监听器以
on开头的属性形式存在于$attrs中(例如$attrs.onCli1) - 组合式 API:使用
defineProps和defineEmits来定义组件的 props 和事件 v-bind="$attrs":现在会同时传递属性和事件监听器
这种变化使得组件通信更加简洁,不再需要分别处理 $attrs 和 $listeners。