“嘿,你的 Props 掉了!”—— Vue 组件通信,我用 $attrs 和 $listeners 就够了


"嘿,你的 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 组件中定义的全局提示)

这个结构很常见,对吧?现在问题来了:

  1. "遥远的 Props" (Props Drilling - 属性钻探) : Top 组件拿到了所有数据,比如 { name: '爱丽丝', age: 30, gender: '女', title: '工程师' }。它需要把 gendertitle 传给 Bottom 组件。按照常规思路,我得先把这些 props 传给 Center,然后 Center 再原封不动地传给 BottomCenter 组件本身根本用不到 gendertitle,它就像个无情的"快递中转站"。如果组件层级再深一点,那简直是一场灾难,维护起来想死的心都有了。😭

  2. "漫长的事件冒泡" : 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'] 

这时,神奇的事情发生了:

  1. Vue 看到爷爷给了 nametitle 两样东西。
  2. Vue 检查了爸爸的需求清单,发现爸爸只要 name
  3. 于是,Vue 就把爸爸不要的 那个 title,自动打包放进了一个叫做 $attrs 的包裹里。这个包裹现在的内容就是 { title: "书" }
  4. 爸爸很省事,他不用关心包裹里到底是什么,直接把整个包裹 (v-bind="$attrs") 递给了你。
  5. 你 (Bottom) 接到包裹后,打开就能直接拿到那本书了!

总结一下 $attrs

  • 是什么? 一个对象,存放父组件传来、但子组件没有通过 props 接收的属性。
  • 怎么用? 使用 v-bind="$attrs",把这个"包裹"原封不动地传给下一个组件。
  • 核心作用: 让中间组件(爸爸)当一个"甩手掌柜",不用一个个去声明和传递他自己根本用不上的属性,代码超级清爽!✨

📞 解释 vm.$listeners:一部"直线电话"

一句话解释: $listeners 就是一部电话,里面存着所有爷爷留下的、可以直接联系到他的电话号码

场景: 你 (Bottom) 在房间里玩,突然想跟爷爷 (Top) 说话(比如触发一个 showAlert 事件)。

按照老规矩,你得先喊爸爸 (@showAlert),然后爸爸再跑去告诉爷爷。太麻烦了!

现在,爷爷 (Top) 很时髦,他直接在爸爸 (Center) 那里装了一部"直线电话",上面有自己的联系方式 (@show-alert="handleShowAlert")。

  1. 爸爸 (Center) 自己用不着这部电话,他很忙。
  2. 于是,Vue 把所有这些"电话号码"(事件监听器),都自动存到了一个叫 $listeners 的电话本里。这个电话本现在的内容是 { 'show-alert': function() {...} }
  3. 爸爸把整部电话 (v-on="$listeners") 直接搬到了你的房间。
  4. 现在,你想跟爷爷说话时,直接从电话本 (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>

划重点 & 踩坑经验

  1. $attrs 的内容$attrs 对象包含了所有从父组件传入、但没有在当前组件的 props 中声明 的属性。在 Center 组件里,我们声明了 nameage,所以 $attrs 里就只剩下 { gender, title } 了。非常干净!
  2. v-bind="$attrs" : 这是语法糖,意思是"把 $attrs 对象里的所有键值对,都作为属性绑定到 Bottom 组件上"。相当于 <Bottom :gender="女" :title="软件工程师" />
  3. inheritAttrs: false (高能预警!🚨) : 这是我当年踩过的最大的坑!如果不设置这个,$attrs 里的 gendertitle 会被 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 组件完全不知道 gendertitle 的存在,它只是做了一个优雅的传递者。代码是不是清爽多了?😎

第二站:$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 事件啦,试试这对"黄金搭档"吧!

好了,今天就聊到这里。希望对大家有帮助,代码敲得开心!👋


相关推荐
小公主12 分钟前
还在等后端接口?vite-plugin-mock 教你前端自造接口跑起来!
前端
Ryan今天学习了吗12 分钟前
💥总结你需要知道有关 JSON 的一切
前端·javascript
1024小神14 分钟前
cocos控制角色玩家飞机只可以在一定范围内移动,不能越界
前端·javascript
晴殇i14 分钟前
Ultracite:告别 ESLint 和 Prettier,迎接 AI 时代的代码格式化新标准
前端·程序员·前端框架
拾光拾趣录14 分钟前
组合总和:深度解析电商促销系统的核心算法实践
前端·算法
三年三月15 分钟前
022-自定义顶点颜色实现渐变
前端·three.js
1024小神18 分钟前
cocos开发2d游戏的时候,模拟背景无限循环移动的思路和实现方法
前端·javascript
我想说一句19 分钟前
React性能优化:深入理解useMemo、useCallback与memo
前端·前端框架
顾辰呀28 分钟前
css flex 一行2个元素 不能挤压空间
前端·css·css3
潜心专研的小张同学29 分钟前
vue3实现高性能pdf预览器功能可行性方案及实践(pdfjs-dist5.x插件使用及自定义修改)
前端·vue.js