目录
- 一、原理
- 二、使用方法
- 三、优缺点
- 优点
- 缺点
- 四、使用注意事项
- 具体代码
- 参考:
一、原理
在Vue中,事件总线(Event Bus)是一种可实现任意组件间通信的通信方式。
要实现这个功能必须满足两点要求:
(1)所有组件都能看到它;
(2)可以进行事件的监听;
对于第一个要求,Vue通过内置关系VueComponent.prototype.__proto__ === Vue.prototype,使组件实例对象可以访问到Vue原型上的属性和方法,所以只需把事件总线放在Vue的原型对象上,它就可以被所有组件访问。
至于第二个要求,Vue在原型对象上定义了$on、$emit、$off等方法,用于实现事件监听。基本原理是基于消息订阅发布:调用$on方法时,将函数存在以事件名为key的数组里;当调用$emit时,获取相应数组里的所有函数,并逐个执行。
源码位置:https://github.com/vuejs/vue/blob/main/src/core/instance/events.ts
所以事件总线就是一个定义在Vue原型对象上的Vue实例。
new Vue({el: '#app',render: h => h(App),beforeCreate() {Vue.prototype.$bus = this;}
})
二、使用方法
一个A组件想给B组件发送数据的场景:
A组件:
// A组件想发送数据,则触发事件并传递参数,参数可以是零个到多个
this.$bus.$emit(事件名, 参数);
B组件:
// B组件想接收数据,则在B组件中给$bus绑定自定义事件,事件的回调留在B组件自身
this.$bus.$on(事件名, 回调函数);
// 需要在beforeDestory钩子中解绑事件,避免内存泄露
this.$bus.$off(事件名, 回调函数);
三、优缺点
优点
- 任意组件间通信
- 组件解耦
通过使用事件总线,可以将组件之间的直接依赖关系解耦,使组件更加独立和可复用。组件只需要关注自身的功能,而不用关心其他组件的实现细节。
缺点
- 只能被动接收数据,不能随时获取状态
如果需要随时获取状态,可以使用状态管理工具Vuex或者Pinia。 - 代码难以调试、数据流向难以追踪
在大型应用中,事件总线的滥用可能导致组件之间的关系变得混乱,导致追踪代码执行流程和调试变得更加困难。 - 潜在的性能问题
大量的全局事件监听和触发可能导致性能问题,尤其是在频繁触发事件的情况下。
四、使用注意事项
- 避免事件命名冲突
由于事件总线是一个全局的对象,为避免事件命名冲突导致错误触发/解绑事件,建议使用具唯一性的命名空间前缀区分不同的事件。 - 避免错误解绑事件
在解绑事件时,一定要带上事件名和相应的回调函数,否则可能会错误解绑其他组件的事件,影响其他组件的正常运行。
// 仅将回调函数与事件名解绑
this.$bus.$off(事件名, 回调函数);
// 解绑事件名下的所有事件
this.$bus.$off(事件名);
// 解绑所有事件
this.$bus.$off();
- 回调函数不能是匿名函数
匿名函数会导致事件无法正常被解绑
- 没有解绑事件,可能导致内存泄露
当组件被销毁时,相关DOM结点已经从DOM树分离出来了,但是还有绑定的事件指向它,导致这些DOM结点无法被垃圾回收,一直在内存里面,就会引发内存泄露。
所以在组件销毁时,可以在组件的beforeDestroy钩子中使用$off方法解绑事件,以防止内存泄漏。
没有解绑时:
正常解绑时:
- 注意事件的传参
在发送事件时,可以通过参数传递数据。但请确保传递的数据是简单且不可变的,避免直接传递引用类型的数据,以免造成数据不一致或意外的修改。
receive(receiveData) {this.receiveData = receiveData;// 在接收的组件中修改了对象中的值,发送的组件中的值也会改变this.receiveData.text = '111';},
- 慎用全局事件
全局事件有很大的便利性,但也容易造成不可预测的问题。在使用事件线时,尽量避免滥用全局事件,可以考虑使用更明确的通信方式。
(1)props和自定义事件应该是父子间通信的首选;
(2)兄弟节点通信可通过它们的父节点进行;
(3)隔代组件通信可以使用provide/inject。它可以避免“prop逐级透传”问题,即prop需要通过许多层级的组件传递下去,但这些组件本身可能并不需要那些prop。
(4)全局共享的数据管理,一般使用Pinia或Vuex等工具。
具体代码
main.js
import Vue from 'vue'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'Vue.config.productionTip = false
Vue.use(ElementUI)new Vue({el: '#app',render: h => h(App),beforeCreate() {Vue.prototype.iBus = this;}
})
App.vue(用v-if就可以触发组件销毁,不一定要用tab)
<template><div><el-tabs v-model="editableTabsValue" type="card" closable @tab-remove="closeTab"><el-tab-panev-for="item in editableTabs":key="item.name":label="item.title":name="item.name"><send-tab v-if="item.name === 'sendTab'"/><receive-tab v-else/></el-tab-pane></el-tabs></div>
</template><script>import ReceiveTab from "@/components/ReceiveTab.vue";
import SendTab from "@/components/SendTab.vue";export default {name: 'App',components: {SendTab, ReceiveTab},data() {return {editableTabsValue: 'sendTab',editableTabs: [{title: '发送页面',name: 'sendTab',},{title: '接收页面',name: 'receiveTab',},],tabIndex: 1,}},methods: {closeTab(targetName) {let tabs = this.editableTabs;let activeName = this.editableTabsValue;if (activeName === targetName) {tabs.forEach((tab, index) => {if (tab.name === targetName) {let nextTab = tabs[index + 1] || tabs[index - 1];if (nextTab) {activeName = nextTab.name;}}});}this.editableTabsValue = activeName;this.editableTabs = tabs.filter(tab => tab.name !== targetName);}}
}
</script>
SendTab.vue
<script>
export default {name: "SendTab",data() {return {input: {text: '',},}},methods: {send() {console.log('--------------emit', this.iBus._events)this.iBus.$emit('bus-demo', this.input, 'data2');},},beforeDestroy() {console.log('--------------发送组件的beforeDestroy')},
}
</script><template><div><el-input style="width: 250px" v-model="input.text"/><el-button type="primary" @click="send">发送数据</el-button></div>
</template>
ReceiveTab.vue
<script>
export default {name: "ReceiveTab",data() {return {receiveData: null,}},created() {this.iBus.$on('bus-demo', this.receive);// this.iBus.$on('anon-func', () => {// console.log('---------------匿名函数')// });console.log('--------------on', this.iBus._events)},beforeDestroy() {this.iBus.$off('bus-demo', this.receive);// this.iBus.$off('anon-func', () => {// console.log('---------------匿名函数')// });console.log('--------------接收组件的beforeDestroy')},methods: {receive(receiveData, otherData) {this.receiveData = receiveData;// this.receiveData.text = '111';console.log('---------第二个参数', otherData)},},
}
</script><template><span>接收到的数据:{{ receiveData }}</span>
</template>
参考:
- Vue3迁移指南-事件总线
- 尚硅谷Vue教程-84/85事件总线
- 【Vue知识】$on和$emit的实现原理
- 详解Vue事件总线的原理与应用:EventBus
- 一个Vue页面的内存泄露分析