译:Vue.js——为什么事件总线(event bus)是一种不好的实践

在研究Vue组件通信的过程中,了解到事件总线(event bus)的概念,直觉上不太认同这种方式。

找到了一篇文章,正是我所想,译文佐证。

原文链接:Vue.js: why event bus is bad idea

2019年2月,我写了一篇关于Vue.js全局事件总线的文章。

事件总线这种模式,应用于一种很常见的场景:组件间传递数据,不止是父传子,还有子传父(译注:兄弟组件传值也适用)。

事件总线还能从父组件触发子组件的事件、调用子组件的方法。

然而,由于很多原因,它并不是一种好的模式,如果我们有其他选项,应该尽量避免使用事件总线。

以我多年的经验来说:事件总线是一种反模式,不应该使用它。为什么?

在这篇文章中,我会试着用最简单的方式来解释。当然,欢迎大家在评论区讨论、写下你的想法和实践。

作为回顾,我们看一下事件总线如何使用,以及它是怎么运行的:在一个单独的文件中创建新的Vue实例,导出这个Vue实例,然后,在任意组件中导入,使用$emit$on方法来声明事件。以下是一个非常简单的例子:

eventBus.js
1
2
import Vue from 'vue'
export const eventBus = new Vue()
组件A
1
2
3
4
5
6
7
8
import {eventBus} from '@/global/eventBus'
(…)
methods: {
myMethod (data) {
// Do something
eventBus.$emit('my-global-event', data)
}
}
组件B
1
2
3
4
5
6
7
import {eventBus} from '@/global/eventBus'
(…)

// Or created, or beforeMount
mounted () {
eventBus.$on('my-global-event', this.actionForGlobalEvent)
}

这是最简单的例子了,它运行很正常:创建了事件总线(新的Vue实例),在组件中导入总线,然后设置触发事件。简单、清晰、易读。那么,事件总线有什么不好吗?

事件总线的问题

首先,命名问题。一个事件总线实例不区分任何命名空间,因此有可能会出现事件重名的问题。

当然,可以创建多个事件总线实例,比如同一类组件创建一个。但当应用规模增大时,会有更大的问题。

也可以定义一套命名规则,例如:myComponent/myEventmycomponent_myevent等等。这当然是可行的,但我们在编码时必须记住这个规则,我们的整个团队必须记住这个规则。这还不是我不喜欢事件总线最主要的原因。

最大的问题是,事件的定义$on,我们用它来创建事件,但还有$off呢!我们还必须记住它,在组件beforeDestory生命周期销毁事件。

为什么?如果不销毁,事件绑定的方法仍然会被触发,即便组件被销毁了。

因为我们用的是全局的事件总线,它仍然在内存中运行着。所以我们必须一直记得销毁事件——当应用规模扩展时,就会造成非常严重的问题,最终难以调试和维护。

它在实际使用中会造成什么问题?按我的经验来描述:有一个父组件List,展示许多子组件。点击子组件,跳转到子组件详情,点击返回,返回父组件。父子都使用事件总线来双向传递数据、事件。同时,子组件还有孙组件,孙组件中的方法也会触发全局事件——传递给List(也就是根节点)。问题来了:如果我们忘了用$off,跳转到了一个子组件,回到父组件,跳转到第二个子组件,再回到父组件,跳转到第三个子组件,然后触发全局事件…List接收到的事件不是一次,而是三次传递了不同数据的事件!这样是有问题的。

用Vuex替代事件总线

当然,如果一直记得用$off也可以,但这并非可控的做法,非常容易漏掉。

我推荐的最佳选项是用Vuex替代事件总线。

它提供了命名空间,重名不再是问题。

它的状态清晰、明确,我们很清楚系统发生了什么。

它提供了dispatch、action和commit,很容易了解数据的变化。

如果我们移除或销毁了组件,Vuex中的变化不会再触发任何方法:有一个例外,就是Vuex内部的push函数,你必须直接使用它。

总之,Vuex清晰、简洁,给你更好的控制手段。