一、什么是发布-订阅模式?
发布-订阅模式 是一种对象间通信的设计模式,包含三个核心角色:
- 发布者(Publisher):发出消息(事件)
- 订阅者(Subscriber):监听并处理消息
- 事件中心(Event Channel):中介,负责消息的注册与分发
类比理解
想象一个"广播电台":
- 电台(事件中心)负责广播
- 主持人(发布者)说:"现在是北京时间 8 点整"
- 听众(订阅者)听到后,各自执行动作(起床、煮咖啡、打卡)
✅ 发布者无需知道谁在听,订阅者也无需知道谁在说,实现解耦。
二、核心概念:订阅、发布、取消订阅
| 操作 | 方法 | 说明 |
|---|---|---|
| 订阅 | on(event, callback) |
注册事件监听器 |
| 发布 | emit(event, ...args) |
触发事件,传递数据 |
| 取消订阅 | off(event, callback) |
移除监听,防止内存泄漏 |
三、在 Vue 中的实现方式
1. Vue 2:使用 Vue 实例作为事件总线
Vue 2 的实例本身实现了事件接口($on、$emit、$off),可直接用作事件中心。
(1) 创建 EventBus
javascript
// utils/eventBus.js
import Vue from 'vue'
// 创建一个 Vue 实例作为全局事件中心
const EventBus = new Vue()
export default EventBus
(2) 订阅消息(在组件中)
html
<!-- NotificationBar.vue -->
<script>
import EventBus from '@/utils/eventBus'
export default {
data() {
return {
message: ''
}
},
created() {
// 订阅 'show-notification' 事件
EventBus.$on('show-notification', (msg, type) => {
this.message = `[${type}] ${msg}`
this.show()
})
},
beforeDestroy() {
// 组件销毁时取消订阅,避免内存泄漏
EventBus.$off('show-notification')
},
methods: {
show() {
// 显示通知
console.log(this.message)
}
}
}
</script>
(3) 发布消息
html
<!-- LoginButton.vue -->
<script>
import EventBus from '@/utils/eventBus'
export default {
methods: {
login() {
// 发布登录成功事件
EventBus.$emit('user-logged-in', { id: 1, name: 'Alice' })
// 发布全局通知
EventBus.$emit('show-notification', '登录成功!', 'success')
}
}
}
</script>
✅ 实现了跨组件通信,但需手动管理生命周期。
2. Vue 3:使用 mitt 库(推荐)
Vue 3 移除了实例上的 $on、$off 方法,因此不能再使用 Vue 实例作为事件总线。
(1) 安装 mitt
bash
npm install mitt
mitt 是一个超轻量(<200B)的事件发射器,API 简洁,完美替代。
(2) 创建全局事件中心
javascript
// utils/eventBus.js
import mitt from 'mitt'
// 创建事件中心
const EventBus = mitt()
export default EventBus
(3) 在 Vue 3 组件中使用
html
<!-- NotificationBar.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import EventBus from '@/utils/eventBus'
// 订阅事件
const handleNotification = (data) => {
console.log(`[通知] ${data.type}: ${data.msg}`)
}
onMounted(() => {
EventBus.on('show-notification', handleNotification)
})
// 取消订阅
onUnmounted(() => {
EventBus.off('show-notification', handleNotification)
})
</script>
html
<!-- LoginButton.vue -->
<script setup>
import EventBus from '@/utils/eventBus'
function login() {
// 发布事件
EventBus.emit('user-logged-in', { id: 1, name: 'Bob' })
EventBus.emit('show-notification', {
msg: '登录成功!',
type: 'success'
})
}
</script>
<template>
<button @click="login">登录</button>
</template>
✅
mitt支持 TypeScript,API 简洁:on、emit、off、clear。
四、手写一个简易的 Event Hub
为了理解原理,我们来实现一个极简版的事件中心:
javascript
// utils/EventHub.js
class EventHub {
constructor() {
this.events = {}
}
// 订阅
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
// 发布
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(...args))
}
}
// 取消订阅
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
// 清除所有
clear(event) {
if (event) {
delete this.events[event]
} else {
this.events = {}
}
}
}
export default new EventHub()
✅ 核心就是维护一个事件-回调函数的映射表。
五、发布-订阅 vs 其他通信方式
| 通信方式 | 方向 | 耦合度 | 适用场景 |
|---|---|---|---|
| 发布-订阅 | 任意 | 低 | 广播通知、解耦组件 |
| props / emit | 父↔子 | 高 | 紧密相关的父子组件 |
| Pinia | 任意 | 中 | 共享状态、复杂逻辑 |
| Provide/Inject | 祖→孙 | 中 | 主题、配置传递 |
📌 选择建议:
- 状态共享 → Pinia
- 简单通知 → mitt
- 祖先传值 → provide/inject
六、最佳实践与注意事项
✅ 1. 使用语义化事件名
javascript
// ❌ 避免模糊命名
EventBus.emit('click')
// ✅ 使用前缀和语义化命名
EventBus.emit('user:login-success', user)
EventBus.emit('cart:item-added', item)
✅ 2. 务必取消订阅
javascript
// Vue 2
beforeDestroy() {
EventBus.$off('event-name')
}
// Vue 3 Composition API
onUnmounted(() => {
EventBus.off('event-name', handler)
})
❌ 忘记
off会导致内存泄漏 和重复触发。
✅ 3. 避免过度使用
- 不要用事件总线代替状态管理
- 复杂状态同步 → 使用 Pinia
- 事件过多 → 考虑模块化事件中心
✅ 4. TypeScript 支持
TypeScript
// types/events.ts
type Events = {
'user:login': (user: User) => void
'cart:update': (count: number) => void
'app:loading': (status: boolean) => void
}
// utils/eventBus.ts
import mitt from 'mitt'
export const useEventBus = () => mitt<Events>()
✅ 类型安全,IDE 自动提示。
七、现代替代方案:Pinia + Actions
对于大多数场景,Pinia 是更优解:
TypeScript
// stores/appStore.ts
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
user: null,
cartCount: 0,
isLoading: false
}),
actions: {
login(user) {
this.user = user
// 其他组件自动响应
},
addToCart(item) {
this.cartCount++
},
setLoading(status) {
this.isLoading = status
}
}
})
html
<!-- 任何组件 -->
<script setup>
import { useAppStore } from '@/stores/appStore'
const appStore = useAppStore()
// 直接调用 action
appStore.login(userData)
</script>
✅ 响应式、可调试、支持 SSR。
八、总结
| 核心点 | 说明 |
|---|---|
| 本质 | 解耦的事件驱动通信 |
| 角色 | 发布者、订阅者、事件中心 |
| Vue 2 | new Vue() 作为 EventBus |
| Vue 3 | 使用 mitt 库 |
| 优点 | 灵活、解耦、轻量 |
| 缺点 | 难以追踪、易内存泄漏 |
| 推荐 | 简单通知用 mitt,状态用 Pinia |
📌 一句话总结 :
发布-订阅是"消息广播",而 Pinia 是"状态中心"------根据场景选择合适的工具。
九、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!