Vue基础知识-脚手架开发-任意组件通信-事件总线($bus)与消息订阅发布(pubsub-js)
一、项目概况与结构
本案例通过「School 组件」和「Student 组件」(兄弟关系,共同父组件是 App)演示通信:
- 功能 1:School 组件点击按钮,通过「事件总线」传递消息给 Student 组件;
- 功能 2:School 组件点击按钮,通过「pubsub-js」传递消息给 Student 组件;
- 核心:两种方案均实现 “School 发消息,Student 收消息” 的兄弟通信逻辑。
项目结构:
src/
├─ components/ # 兄弟组件目录
│ ├─ School.vue # 消息发送方
│ └─ Student.vue # 消息接收方
├─ App.vue # 父组件(仅作为容器,不参与通信)
└─ main.js # 入口文件(事件总线初始化关键)
二、完整源码实现
1. 入口文件:main.js(事件总线初始化)
事件总线($bus)的核心是在 Vue 原型上挂载一个全新的 Vue 实例,作为“事件载体”(所有组件都能通过原型链访问到这个实例)。
import Vue from 'vue'import App from './App.vue'Vue.config.productionTip = false/* /* 前置知识:1 $on,$emit来自Vue.prototype2 VueComponent.prototype.prototype === Vue.prototype
*/
Vue.prototype.$bus = new Vue()/* 此时,Vue原型上的$bus是全新的vm。又因为VueComponent.prototype.prototype === Vue.prototype。因此任何vc都可以使用this.$bus.$on()往$bus(沿着原型链在全新的vm)上绑定自定义事件并给回调函数同时,任何vc可以使用this.$bus.$emit来触发$bus上的自定义事件。实现任意组件间的通信注:一个事件可以对应多个回调函数。当事件发生时,所有注册了该事件的回调函数都会被依次调用。但一般需一一对应
*/
new Vue({render: h => h(App),beforeCreate(){}
}).$mount('#app')
2. 兄弟组件 1:School.vue(消息发送方)
负责通过两种方案发送消息:
- 事件总线:通过
this.$bus.$emit(事件名, 数据)
触发事件; - pubsub-js:通过
pubsub.publish(消息名, 数据)
发布消息。
<template><div class="demo"><h2>学校名称:{{name}}</h2><input type="button" value="兄弟相传School-Studnet(事件总线实现)" @click="setMsg"/><!--消息订阅与发布:npm install pubsub-js--><input type="button" value="兄弟相传School-Studnet(消息订阅/发布实现)" @click="setMsg2"/></div>
</template>
<script>//导入import pubsub from 'pubsub-js'export default {name:'School',data(){return {name:'北京大学',msg:'你好呀,$bus',msg2:'你好呀,pubsub-js'}},methods:{setMsg(){this.$bus.$emit('getMsg',this.msg)},setMsg2(){pubsub.publish('getMsg2',this.msg2)},},}
</script><style scoped>.demo{background-color: red;padding:10px;}
</style>
3. 兄弟组件 2:Student.vue(消息接收方)
负责通过两种方案接收消息,并在组件销毁前 “解绑事件 / 取消订阅”(防止内存泄漏):
- 事件总线:通过
this.$bus.$on(事件名, 回调)
绑定事件; - pubsub-js:通过
pubsub.subscribe(消息名, 回调)
订阅消息。
<template><div class="demo"><h2>学生名称:{{name}}</h2><h2>msg:{{msg}}</h2><h2>msg2:{{msg2}}</h2></div>
</template>
<script>//导入import pubsub from 'pubsub-js'export default { name:'Student',data(){return {name:'张三',msg:'',msg2:''}},methods:{getMsg(msg){this.msg = msg},//注意第一个参数是消息名。getMsg2(msgName,msg2){this.msg2 = msg2}},mounted(){this.$bus.$on('getMsg',this.getMsg)this.pubId = pubsub.subscribe('getMsg2',this.getMsg2)//写成匿名函要求使用箭头函数},beforeDestroy(){//记得解绑事件,因为是公共的vue实例对象。自定义事件在组件实例对象。组件实例对象销毁则自定义事件自然销毁this.$bus.$off('getMsg')//解绑getMsg事件下所有回调;//this.$bus.$off('getMsg',this.getMsg)仅解绑该事件名下与 “回调函数” 匹配的回调//取消订阅pubsub.unsubscribe(pubId)}}
</script><style scoped>.demo{background-color: orange;padding: 10px;margin-top: 30px;}
</style>
4. 父组件:App.vue(仅作为容器)
App 组件仅负责渲染两个兄弟组件,不参与通信逻辑(体现兄弟组件 “直接通信” 的特点)。
<template><div class="app"><School /><Student /></div>
</template><script>import School from './components/School' import Student from './components/Student'; export default {name:'App',data() {return {schoolName:'',studentlName:''}},components:{School,Student,},}
</script><style>.app{background-color: gray;padding: 10px;}</style>
三、核心原理解析
1. 方案 1:事件总线($bus)
(1)为什么能实现兄弟通信?
事件总线的核心是全局共享的 Vue 实例(this.$bus
),它具备以下特性:
- Vue 实例自带
$on
(绑定事件)、$emit
(触发事件)、$off
(解绑事件)方法,可作为 “事件容器”; - 通过
Vue.prototype.$bus = new Vue()
,所有组件实例(vc)都能通过原型链访问到这个$bus
(相当于 “全局变量”); - 发送方通过
$bus.$emit
触发事件,接收方通过$bus.$on
绑定事件,$bus
作为中间载体传递消息。
(2)关键步骤(3 步)
- 初始化:Vue.prototype.$bus = new Vue();
- 发送消息:发送方用
this.$bus.$emit(事件名, 数据)
触发事件; - 接收消息:接收方用
this.$bus.$on(事件名, 回调)
绑定事件,回调中处理数据; - 解绑事件:接收方在
beforeDestroy
中用this.$bus.$off
解绑,防止内存泄漏。
2. 方案 2:消息订阅发布(pubsub-js)
(1)什么是 pubsub-js?
pubsub-js 是一个独立的 JavaScript 库,基于 “发布 - 订阅模式”(Publish/Subscribe)实现跨组件通信,核心思想是:
- 发布者(Publisher):发送消息(如 School 组件用
pubsub.publish
); - 订阅者(Subscriber):接收消息(如 Student 组件用
pubsub.subscribe
); - 消息中心:pubsub 库自身作为 “中间件”,管理所有消息的发布与订阅。
(2)关键步骤(4 步)
- 安装依赖:项目根目录执行
npm install pubsub-js --save
; - 导入库:在发送方和接收方组件中
import pubsub from 'pubsub-js'
; - 发布消息:发送方用
pubsub.publish(消息名, 数据)
发布消息; - 订阅消息:接收方用
pubsub.subscribe(消息名, 回调)
订阅消息(回调第一个参数是消息名,第二个是数据); - 取消订阅:接收方在
beforeDestroy
中用pubsub.unsubscribe(订阅ID)
取消,防止内存泄漏。
四、两种方案对比与适用场景
对比维度 | 事件总线($bus) | 消息订阅发布(pubsub-js) |
---|---|---|
依赖 | 无(基于 Vue 原生 API,无需额外安装) | 需安装第三方库(pubsub-js) |
核心载体 | 全局 Vue 实例(this.$bus ) | pubsub 库自身(消息中心) |
语法简洁度 | 高($emit /$on /$off ,Vue 开发者熟悉) | 中(publish /subscribe /unsubscribe ) |
回调参数 | 直接接收数据(如handle(data) ) | 第一个参数是消息名(如handle(name, data) ) |
多消息管理 | 需手动区分事件名,易冲突 | 消息名独立管理,冲突风险低 |
跨框架兼容性 | 仅 Vue(依赖 Vue 实例方法) | 通用(支持 Vue/React/ 原生 JS 等) |
内存泄漏风险 | 高(需手动$off 解绑,否则事件残留) | 中(需保存订阅 ID,用unsubscribe 取消) |
适用场景 | 中小型 Vue 项目,兄弟 / 跨级组件简单通信 | 大型项目,多组件复杂通信(如跨模块) |
五、注意事项
1. 事件总线($bus)
- 事件名冲突:不同组件若用相同事件名(如
'sendMsg'
),可能导致回调函数重复触发,建议用 “组件名 - 方向 - 事件名” 命名(如'school-to-student-bus'
); - 必须解绑事件:
$bus
是全局实例,组件销毁后若不$off
解绑,事件会残留(下次创建组件时会重复绑定,导致回调多次执行); - 避免绑定匿名函数:若用
this.$bus.$on('事件名', (data) => { ... })
,解绑时无法定位到具体回调(需用命名函数,如this.handleBusMsg
)。
2. pubsub-js
- 回调参数顺序:订阅回调的第一个参数是 “消息名”,第二个才是 “数据”(新手易忽略,导致
data
拿到消息名); - 保存订阅 ID:
pubsub.subscribe
会返回一个唯一 ID(如1
、2
),必须用this.pubsubId
保存,否则无法精准取消订阅(pubsub.unsubscribe
需传入该 ID); - 取消订阅时机:必须在
beforeDestroy
中取消订阅(组件销毁后若不取消,消息触发时会继续执行回调,导致内存泄漏)。
六、总结
- 事件总线($bus):Vue 原生方案,轻量简洁,适合中小型 Vue 项目的兄弟 / 跨级通信,核心是 “全局 Vue 实例作为事件载体”,需注意事件名冲突和手动解绑;
- pubsub-js:第三方通用方案,支持跨框架,适合大型项目的复杂通信,核心是 “发布 - 订阅模式”,需注意回调参数顺序和订阅 ID 管理。