Vue中的事件总线(EventBus)是什么?它有什么优点和缺点?
作为一名使用Vue的前端开发者,有时候会听到事件总线(EventBus)这个名词。但可能是我入行比较晚,我在Vue网站中并没有看到过事件总线的介绍,在项目中也没有使用过。那究竟什么是事件总线?事件总线可以解决什么问题?
事件总线简介
事件总线是一种组件通信方式,用于在工程的中的任意组件中进行事件触发和数据传递。
通过在全局创建一个事件总线,所有组件(无论他们的关系是父子还是兄弟还是不相关)都可以使用同一个总线发送事件和监听事件,传输数据。这样通信就可以不受组件间关系限制,实现灵活的通信能力。
Vue2实现事件总线
创建总线
首先创建一个Vue2项目,可以使用Vue CLI
。然后在src/main.js
中创建一个事件总线。创建的方式有两种:
- 新创建一个Vue实例
import Vue from 'vue'
import App from './App.vue'
Vue.prototype.$EventBus = new Vue()
new Vue({ render: h => h(App), }).$mount('#app')
- 使用已有的Vue实例
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App),
beforeCreate() { Vue.prototype.$EventBus = this; }
}).$mount('#app')
触发/接收事件
我们假设有两个组件A和B,A触发事件,B接收事件。
- 组件A
<template>
<div> <p @click="add"> 点击增加 </p> </div>
</template>
<script>
export default {
name: 'Add',
data() { return { sum: 1, addNum: 1 } },
methods: {
add() {
this.sum += this.addNum;
this.addNum++;
this.$EventBus.$emit('add', this.sum);
}
}
}
</script>
- 组件B
<template>
<div> <h1>收到数据: {{ sum }}</h1> </div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
sum: 1,
listenFun: (sum) => { this.sum = sum; }
}
},
mounted() {
this.$EventBus.$on('add', this.listenFun)
},
beforeDestroy() {
this.$EventBus.$off('add', this.listenFun)
},
}
</script>
可以看到事件总线的实现方式实际上非常简单,就是把一个Vue实例挂载为一个全局属性,在这个实例上触发事件,监听事件即可。如果不需要监听时,要记得销毁监听事件。
其它组件通信方式
Vue2有很多组件间的通信方式,这里总结一下:
- 组件Props 父组件向子组件传递数据
- 组件事件Emit 子组件触发事件;父组件监听事件,接收数据
- 组件v-model 通过props和事件实现父子组件数据的双向绑定
- 依赖注入 父组件向后代组件传递数据
- Attributes 没有被组件声明为props或emits的属性;父组件向子组件传递数据
- 状态管理 全局共享的数据管理,一般使用Pinia或者Vuex等工具
- 事件总线 全局组件共享的事件管理
- 模板引用ref 父组件主动调用子组件方法,可传递数据
- 其它方式 可以存放数据的公共位置,比如Storage, Window等。
事件总线的优缺点
通过事件总线的实现,我们可以了解到事件总线可以非常简单的实现全局组件共享的事件管理,传递数据等。既然如此简单,那Vue为什么没有推荐作为官方的组件通信方式?为什么即使Vue官方并无推荐,但却有很多开发者使用事件总线。我们结合上面的其它组件通信方式,来讨论下事件总线的优缺点。
优点
- 实现全局任意组件共享的数据传输
查看上面的通信方式,我们可以看到Vue提供的大部分方式都有组件关系的限制,大部分是父组件向子组件向后代组件之间传递。而事件总线却没有任何限制。 - 实现非常简单
使用状态管理工具也可以实现数据传递,但是这些工具都要引入依赖库,有自己的使用方式。虽然并不麻烦,但是都没有事件总线使用这么简单。 - 全局的事件管理器
组件通信除了传递数据,另一个作用是实时触发事件,针对事件进行操作。查看上面的组件通信方式,我们发现除事件总线外,全局通信只是数据的传递,没有事件的触发。虽然通过监听状态管理和Storage数据等,可以变相实现事件的管理,但是并没有事件总线清晰和直接。
缺点
- 事件监听只能被动接收数据,不能随时获取状态
如果需要随时获取状态,显然还是状态管理工具更适合。 - vue3不提供事件总线能力
在vue3中$on $off
等实例方法已被移除,组件实例不再实现事件触发接口。官方推荐使用 mitt 等外部工具。
还有使用不慎带来的很多问题。例如:
- 事件名共享同一个命名空间
- 不销毁事件监听器
如果在不使用后忘记销毁事件监听器,会造成难以排查的Bug或者引发性能问题。 - 误销毁同名事件其它监听器
比如多个组件都监听了同一事件'add'。其中某个组件销毁了'add'事件下的所有监听器this.$EventBus.$off('add')
,就会影响其他的组件。 - 其它问题 例如调试困难,耦合性高等等。
总结
事件总线作为一种全局的组件通信方法,符合订阅发布模式,由于其简单有效的使用方式,受到部分开发者的欢迎。但是由于各种使用不慎和维护带来的问题,官方和许多开发者也不推荐使用:
在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。根据具体情况来看,有多种事件总线的替代方案. —— Vue3迁移指南
但是事件总线就完全不能使用么?也并不是。首先上面说的各种使用问题,可以通过预先制定开发规范+严格代码审核解决。其次,查看上面的通信方法,其实Vue并没有直接的全局事件通知方式,作为一种全局事件通知工具,还是有它独特的作用。是否可以使用,还是要具体问题具体分析。
参考
- Vue3迁移指南 事件API
https://v3-migration.vuejs.org/zh/breaking-changes/events-api.html - mitt —— Tiny 200b functional event emitter / pubsub.
https://github.com/developit/mitt