一、什么是全局事件总线?
全局事件总线 是一个独立的事件中心 ,任何组件都可以向它发送(emit)事件 ,也可以监听(on)事件,从而实现任意组件间的通信。
它类似于一个"广播站":
- 组件 A 发送消息:"用户已登录"
- 组件 B、C、D 监听到后,执行相应逻辑
二、Vue 2 中的事件总线实现
在 Vue 2 中,可以利用 Vue 实例本身就是一个事件系统的特点,创建一个全局的 Vue 实例作为事件总线。
1. 创建 EventBus
javascript
// utils/eventBus.js
import Vue from 'vue'
// 创建一个 Vue 实例作为事件总线
const EventBus = new Vue()
export default EventBus
2. 组件 A:发送事件
html
<!-- LoginButton.vue -->
<template>
<button @click="login">登录</button>
</template>
<script>
import eventBus from '@/utils/eventBus'
export default {
methods: {
login() {
// 模拟登录成功
const user = { id: 1, name: 'Alice' }
// 发送事件
eventBus.$emit('user-logged-in', user)
// 也可携带多个参数
eventBus.$emit('show-toast', '登录成功!', 'success')
}
}
}
</script>
3. 组件 B:监听事件
html
<!-- Sidebar.vue -->
<template>
<div>
<p>欢迎,{{ userName }}</p>
</div>
</template>
<script>
import eventBus from '@/utils/eventBus'
export default {
data() {
return {
userName: '游客'
}
},
created() {
// 监听登录事件
eventBus.$on('user-logged-in', (user) => {
this.userName = user.name
})
// 监听提示事件
eventBus.$on('show-toast', (text, type) => {
this.showToast(text, type)
})
},
methods: {
showToast(text, type) {
// 显示提示(可集成 toast 插件)
console.log(`[${type}] ${text}`)
}
},
// 组件销毁时,务必解绑事件,避免内存泄漏!
beforeDestroy() {
eventBus.$off('user-logged-in')
eventBus.$off('show-toast')
}
}
</script>
✅ 实现了跨组件通信。
三、Vue 3 中的挑战与替代方案
Vue 3 移除了 $on、$off、$once 等实例方法,因此 无法再使用 Vue 实例作为事件总线。
解决方案:使用 mitt 库
mitt 是一个超轻量(<200B)的事件发射器,完美替代 Vue 2 的 EventBus。
1. 安装 mitt
bash
npm install mitt
2. 创建全局事件总线
javascript
// utils/eventBus.js
import mitt from 'mitt'
// 创建事件中心
const EventBus = mitt()
export default EventBus
3. 组件中使用(Vue 3 + Composition API)
html
<!-- LoginButton.vue -->
<script setup>
import { ref } from 'vue'
import EventBus from '@/utils/eventBus'
const user = ref(null)
function login() {
user.value = { id: 1, name: 'Bob' }
// 发送事件
EventBus.emit('user-logged-in', user.value)
EventBus.emit('show-toast', '登录成功!', 'success')
}
</script>
<template>
<button @click="login">登录</button>
</template>
html
<!-- Sidebar.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import EventBus from '@/utils/eventBus'
const userName = ref('游客')
// 监听事件
const handleLogin = (user) => {
userName.value = user.name
}
const handleToast = (text, type) => {
console.log(`[${type}] ${text}`)
}
onMounted(() => {
EventBus.on('user-logged-in', handleLogin)
EventBus.on('show-toast', handleToast)
})
// 组件卸载时解绑
onUnmounted(() => {
EventBus.off('user-logged-in', handleLogin)
EventBus.off('show-toast', handleToast)
})
</script>
<template>
<div>
<p>欢迎,{{ userName }}</p>
</div>
</template>
✅
mittAPI 简洁:on、emit、off、clear。
四、事件总线的优缺点
| 优点 | 缺点 |
|---|---|
| ✅ 灵活:任意组件可通信 | ❌ 难以追踪:事件来源和去向不清晰 |
| ✅ 解耦:发送方无需知道接收方 | ❌ 命名冲突:多个模块使用相同事件名 |
| ✅ 轻量:代码少,上手快 | ❌ 内存泄漏 :忘记 off 会导致事件堆积 |
| ✅ 适合一次性通知 | ❌ 不适合复杂状态管理 |
📌 核心问题:事件总线让代码变得"隐式"和"不可预测",不利于大型项目维护。
五、现代替代方案(推荐 ✅)
虽然事件总线简单,但在现代 Vue 项目中,更推荐以下方案:
1. Pinia(推荐):状态驱动 + 监听
TypeScript
// stores/userStore.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
isLoggedIn: false
}),
actions: {
login(userData) {
this.user = userData
this.isLoggedIn = true
// 状态变化自动通知所有使用该 store 的组件
}
}
})
html
<!-- Sidebar.vue -->
<script setup>
import { useUserStore } from '@/stores/userStore'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const { user, isLoggedIn } = storeToRefs(userStore)
</script>
<template>
<div v-if="isLoggedIn">
欢迎,{{ user.name }}
</div>
</template>
✅ 状态集中管理,响应式更新,调试工具支持。
2. Provide / Inject:祖先 → 后代通信
html
<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// 提供给所有后代
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
html
<!-- AnyChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
<template>
<button @click="toggleTheme">
切换到{{ theme === 'light' ? '深色' : '浅色' }}模式
</button>
</template>
✅ 适合主题、配置等全局状态。
3. 自定义 Hook + mitt(轻量场景)
对于简单的通知(如 toast、modal),可以封装 useEventBus:
TypeScript
// composables/useEventBus.ts
import mitt from 'mitt'
type Events = {
'show-toast': (text: string, type?: string) => void
'open-modal': (id: string) => void
}
export const useEventBus = () => mitt<Events>()
然后在需要的地方使用。
六、何时使用事件总线?
| 场景 | 建议 |
|---|---|
| Vue 2 项目 | ✅ 可使用(注意解绑) |
| Vue 3 项目 | ⚠️ 仅用于简单通知,优先考虑 Pinia |
| 一次性通知 | ✅ 如:show-toast、route-changed |
| 复杂状态同步 | ❌ 使用 Pinia |
| 大型项目 | ❌ 避免,增加维护成本 |
七、总结
| 方案 | 适用场景 | 推荐度 |
|---|---|---|
| EventBus (Vue 2) | Vue 2 项目简单通信 | ⚠️ 仅限老项目 |
| mitt | Vue 3 轻量事件通知 | ✅ 适合通知类 |
| Pinia | 状态共享、复杂逻辑 | ✅✅✅ 首选 |
| Provide/Inject | 祖先→后代传递配置 | ✅ 适合主题、语言等 |
📌 核心结论:
- 事件总线是"快捷方式",不是"最佳实践"。
- 在 Vue 3 项目中,优先使用 Pinia 或 Composition API。
- 仅在轻量级、一次性通知 场景下考虑
mitt。
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!