"嘿,你的 Props 掉了!"------ Vue 组件通信,我用 <math xmlns="http://www.w3.org/1998/Math/MathML"> a t t r s 和 attrs 和 </math>attrs和listeners 就够了 😉
大家好,我是你们的老朋友,一个在前端世界里摸爬滚打多年的老兵。今天想和大家唠唠嗑,聊一个我们几乎在每个项目中都会遇到的"烦心事":组件嵌套太深,传个值、触个发咋就这么费劲呢?! 😫
我遇到了什么问题?
还记得那个项目吗?我们正在构建一个高度可定制化的用户中心页面。整个页面被拆分成了多个组件:
scss
App.vue (根组件)
└─ Top.vue (顶层容器,负责获取和管理所有用户数据)
└─ Center.vue (中间布局组件,可能只负责展示用户的姓名和年龄)
└─ Bottom.vue (底层孙子组件,需要展示用户的性别,并且要触发一个在 Top 组件中定义的全局提示)
这个结构很常见,对吧?现在问题来了:
-
"遥远的 Props" (Props Drilling - 属性钻探) :
Top
组件拿到了所有数据,比如{ name: '爱丽丝', age: 30, gender: '女', title: '工程师' }
。它需要把gender
和title
传给Bottom
组件。按照常规思路,我得先把这些 props 传给Center
,然后Center
再原封不动地传给Bottom
。Center
组件本身根本用不到gender
和title
,它就像个无情的"快递中转站"。如果组件层级再深一点,那简直是一场灾难,维护起来想死的心都有了。😭 -
"漫长的事件冒泡" :
Bottom
组件里有个按钮,点击后需要在Top
组件里弹出一个提示框。标准的做法是,Bottom
组件$emit
一个事件,Center
组件监听这个事件,然后自己再$emit
一个同样的事件,最后Top
组件才监听到。这又是一个"快递中转"的故事,代码啰嗦,关系链一长就容易断。
难道就没有更优雅的办法吗?当然有!就在我抓耳挠腮的时候,我想起了 Vue 中那两个低调但极其强大的神器:$attrs
和 $listeners
。那一刻,我恍然大悟!💡

我是如何用 attrs 和 listeners 解决的
官方解释
v2.cn.vuejs.org/v2/api/#vm-...

v2.cn.vuejs.org/v2/api/#vm-...

通俗易懂的解释:
忘掉那些专业的术语,我们用一个收快递的场景来打比方,保证你一听就懂。😉
想象一个三代同堂的家庭:
- 爷爷 (
Top
组件):家里的大 boss,所有好东西都在他那儿。 - 爸爸 (
Center
组件):中间人,负责传递东西。 - 你 (
Bottom
组件):最终接收东西的人。
📦 解释 vm.$attrs:一个"懒人包裹"
一句话解释: $attrs
就是一个包裹,里面装着所有爷爷给的、但爸爸自己不要的东西。
场景: 今天,爷爷 (Top
) 要给你 (Bottom
) 寄两样东西:一个玩具 (:name="玩具"
) 和一本书 (:title="书"
)。
他把这两样东西都交给了爸爸 (Center
)。
但是,爸爸 (Center
) 自己也需要那个玩具 ,所以他在自己的需求清单 (props
) 上写明了:"我要 name
这个玩具"。
javascript
// 爸爸(Center.vue)的需求清单
props: ['name']
这时,神奇的事情发生了:
- Vue 看到爷爷给了
name
和title
两样东西。 - Vue 检查了爸爸的需求清单,发现爸爸只要
name
。 - 于是,Vue 就把爸爸不要的 那个
title
,自动打包放进了一个叫做$attrs
的包裹里。这个包裹现在的内容就是{ title: "书" }
。 - 爸爸很省事,他不用关心包裹里到底是什么,直接把整个包裹 (
v-bind="$attrs"
) 递给了你。 - 你 (
Bottom
) 接到包裹后,打开就能直接拿到那本书了!
总结一下 $attrs
:
- 是什么? 一个对象,存放父组件传来、但子组件没有通过
props
接收的属性。 - 怎么用? 使用
v-bind="$attrs"
,把这个"包裹"原封不动地传给下一个组件。 - 核心作用: 让中间组件(爸爸)当一个"甩手掌柜",不用一个个去声明和传递他自己根本用不上的属性,代码超级清爽!✨
📞 解释 vm.$listeners:一部"直线电话"
一句话解释: $listeners
就是一部电话,里面存着所有爷爷留下的、可以直接联系到他的电话号码。
场景: 你 (Bottom
) 在房间里玩,突然想跟爷爷 (Top
) 说话(比如触发一个 showAlert
事件)。
按照老规矩,你得先喊爸爸 (@showAlert
),然后爸爸再跑去告诉爷爷。太麻烦了!
现在,爷爷 (Top
) 很时髦,他直接在爸爸 (Center
) 那里装了一部"直线电话",上面有自己的联系方式 (@show-alert="handleShowAlert"
)。
- 爸爸 (
Center
) 自己用不着这部电话,他很忙。 - 于是,Vue 把所有这些"电话号码"(事件监听器),都自动存到了一个叫
$listeners
的电话本里。这个电话本现在的内容是{ 'show-alert': function() {...} }
。 - 爸爸把整部电话 (
v-on="$listeners"
) 直接搬到了你的房间。 - 现在,你想跟爷爷说话时,直接从电话本 (
this.$listeners
) 里找到show-alert
这个号码,拨过去就行了!电话直接接通爷爷,全程没爸爸什么事。
第一站:$attrs ------ 优雅地透传属性
顶层组件 Top.vue
这是我们的数据源头,它像往常一样把所有数据传给 Center
。
vue
<!-- Top.vue -->
<template>
<section>
<h2>顶层组件</h2>
<Center
:name="name"
:age="age"
:gender="gender"
:title="title"
@log-message="handleLogMessage"
@show-alert="handleShowAlert"
></Center>
</section>
</template>
<script>
import Center from './Center.vue';
export default {
components: {
Center
},
data() {
return {
name: '爱丽丝',
age: 30,
gender: '女',
title: '软件工程师'
};
},
methods: {
handleLogMessage(message) {
console.log('来自 Bottom 的消息,在 Top 中处理:', message);
},
handleShowAlert() {
alert('来自 Bottom 的警报,在 Top 中处理!');
}
}
};
</script>
<style scoped>
section {
border: 2px solid #42b983;
padding: 15px;
margin: 10px;
border-radius: 8px;
}
</style>
中间组件 Center.vue
(这里的魔法最关键 ✨)
Center
是我们的"快递中转站",但这次我们要让它变得智能起来。
vue
<!-- Center.vue -->
<template>
<section>
<h3>中间组件</h3>
<p>收到的 props: name={{ name }}, age={{ age }}</p>
<Bottom v-bind="$attrs" v-on="$listeners" />
</section>
</template>
<script>
import Bottom from "./Bottom.vue";
export default {
components: {
Bottom,
},
inheritAttrs: false, // 防止 attrs 被应用到此组件的根元素上
props: {
name: {
type: String,
default: "",
},
age: {
type: Number,
default: 0,
},
},
created() {
console.log("Center 组件的 $attrs:", this.$attrs);
console.log("Center 组件的 $listeners:", this.$listeners);
},
};
</script>
<style scoped>
section {
border: 2px solid #f0ad4e;
padding: 15px;
margin: 10px;
border-radius: 8px;
}
</style>
</script>
划重点 & 踩坑经验:
$attrs
的内容 :$attrs
对象包含了所有从父组件传入、但没有在当前组件的props
中声明 的属性。在Center
组件里,我们声明了name
和age
,所以$attrs
里就只剩下{ gender, title }
了。非常干净!v-bind="$attrs"
: 这是语法糖,意思是"把$attrs
对象里的所有键值对,都作为属性绑定到Bottom
组件上"。相当于<Bottom :gender="女" :title="软件工程师" />
。inheritAttrs: false
(高能预警!🚨) : 这是我当年踩过的最大的坑!如果不设置这个,$attrs
里的gender
和title
会被 Vue 自动应用到Center
组件的根元素<section>
上,你的 HTML 会变成<section gender="女" title="软件工程师">
。这不仅污染了 HTML 结构,还可能导致意想不到的样式问题。设置成false
就是告诉 Vue:"我自己手动处理这些属性,你别多管闲事!"
底层组件 Bottom.vue
现在,Bottom
组件可以轻松地拿到它需要的一切。
vue
<!-- Bottom.vue -->
<template>
<section>
<h4>底层组件</h4>
<p>收到的 prop: gender={{ gender }}</p>
<p>收到的 attr: title={{ $attrs.title }}</p>
<button @click="triggerLog">通过 $listeners 记录消息</button>
<button @click="triggerAlert">通过 $listeners 显示警报</button>
</section>
</template>
<script>
export default {
props: {
gender: {
type: String,
default: ''
}
},
mounted() {
console.log('Bottom 组件的 $attrs:', this.$attrs);
console.log('Bottom 组件的 $listeners:', this.$listeners);
},
methods: {
triggerLog() {
// 直接触发顶层父组件的事件
this.$listeners['log-message']('你好,来自底层!');
},
triggerAlert() {
// 直接触发另一个事件
this.$listeners['show-alert']();
}
}
};
</script>
<style scoped>
section {
border: 2px solid #d9534f;
padding: 15px;
margin: 10px;
border-radius: 8px;
}
button {
margin-right: 10px;
}
</style>
看!Bottom
组件可以像接收普通 prop
一样接收 gender
,也可以直接从 $attrs
中读取 title
。整个过程,Center
组件完全不知道 gender
和 title
的存在,它只是做了一个优雅的传递者。代码是不是清爽多了?😎
第二站:$listeners ------ 跨越山海的事件呼唤
回顾 Center.vue
我们已经在 Center
组件里写了 v-on="$listeners"
。
vue
<!-- Center.vue -->
<template>
<!-- ... -->
<Bottom v-bind="$attrs" v-on="$listeners" />
</template>
<script>
export default {
// ...
created() {
console.log("Center 组件的 $listeners:", this.$listeners);
// 输出: { log-message: f(), show-alert: f() }
// 这是 Top 组件传递过来的两个事件监听函数
},
};
</script>
v-on="$listeners"
的作用就是,把 Top
组件绑定在 Center
上的所有事件监听器,原封不动地再绑定到 Bottom
组件上。Center
再次扮演了透明的传递者。
底层组件 Bottom.vue
触发事件
现在,Bottom
组件可以直接调用这些来自 Top
组件的"遥控指令"了。
vue
<!-- Bottom.vue -->
<template>
<section>
<!-- ... -->
<button @click="triggerLog">通过 $listeners 记录消息</button>
<button @click="triggerAlert">通过 $listeners 显示警报</button>
</section>
</template>
<script>
export default {
// ...
methods: {
triggerLog() {
// 直接调用 $listeners 里的方法,就像调用本地方法一样!
this.$listeners['log-message']('你好,来自底层!');
},
triggerAlert() {
this.$listeners['show-alert']();
}
}
};
</script>
看到了吗?Bottom
组件不再需要 $emit
事件让 Center
去中转了。它通过 this.$listeners
直接拿到了 Top
组件的方法引用,并执行了它!这感觉就像 Bottom
组件有了一部"直线电话",可以直接打给 Top
组件,而 Center
组件完全不会被骚扰。📞
这才是真正的"解耦"!
总结一下,什么时候用?
$attrs
和 $listeners
是解决 Vue 2.x 中**"隔代通信"**问题的绝佳方案。
- 用它:当你在封装一些高阶组件,尤其是 UI 库组件时,或者遇到超过三层的组件嵌套时。它能让你的中间层组件保持纯粹,只关注自己的逻辑,而不用当一个笨拙的"快递员"。
- 注意 :在 Vue 3 中,
$listeners
已被移除,它的功能被合并到了$attrs
中。事件监听器现在会作为以on
开头的属性出现在$attrs
对象里。同时,Vue 3 更加推崇使用provide/inject
来处理跨层级的数据共享。但如果你还在维护 Vue 2 项目,$attrs
和$listeners
绝对是你的得力助手!
希望这次的分享能让你对 Vue 组件通信有新的认识。下次再遇到类似的"千层饼"组件结构,别再傻傻地一层层传 props 和 emit 事件啦,试试这对"黄金搭档"吧!
好了,今天就聊到这里。希望对大家有帮助,代码敲得开心!👋