在 Vue 3 中,官方不再内置类似于 Vue 2 中的全局事件总线($on
、$off
、$emit
)功能,而是鼓励开发者使用更显式的方式(如 props
、provide/inject
或外部状态管理库如 Pinia)来处理组件通信。然而,发布-订阅模式(Publish-Subscribe Pattern)仍然是一种非常有效的组件间通信方式,尤其适用于非父子关系的组件通信场景。在 Vue 3 中,我们可以通过自定义实现一个事件总线(Event Bus)来应用发布-订阅模式。
下面我将详细讲解 Vue 3 中发布-订阅模式的实现原理、使用方式,并结合 Vue 3 的特性(如 Composition API)提供示例。
1. Vue 3 中发布-订阅的背景
- Vue 2 的实现:Vue 2 中可以通过
Vue.prototype.$bus = new Vue()
创建一个全局事件总线,利用$emit
发布事件,$on
订阅事件。 - Vue 3 的变化:Vue 3 移除了实例上的
$on
、$off
和$once
方法,因此不能直接用 Vue 实例作为事件总线。 - 替代方案:
- 使用独立的 JavaScript 类实现事件总线。
- 结合 Composition API 或第三方库(如
mitt
、tiny-emitter
)实现。
2. 自定义实现发布-订阅
我们可以通过创建一个简单的 EventBus
类来实现发布-订阅模式,并在 Vue 3 项目中使用。
EventBus 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// eventBus.ts class EventBus { private events: { [key: string]: Array<(...args: any[]) => void> } constructor() { this.events = {} } // 订阅事件 on(eventName: string, callback: (...args: any[]) => void): void { if (!this.events[eventName]) { this.events[eventName] = [] } this.events[eventName].push(callback) } // 发布事件 emit(eventName: string, ...args: any[]): void { if (!this.events[eventName]) { console.warn(`No subscribers to event: ${eventName}`) return } this.events[eventName].forEach(callback => callback(...args)) } // 取消订阅 off(eventName: string, callback: (...args: any[]) => void): void { if (!this.events[eventName]) return this.events[eventName] = this.events[eventName].filter(cd => cd !== callback) if (this.events[eventName].length === 0) { delete this.events[eventName] } } // 一次性订阅 once(eventName: string, callback: (...args: any[]) => void): void { const wrappedCallback = (...args: any[]) => { callback(...args) this.off(eventName, wrappedCallback) } this.on(eventName, wrappedCallback) } } export default new EventBus() |
说明:
on
:订阅事件,将回调函数添加到指定事件。emit
:发布事件,触发所有订阅者的回调。off
:取消订阅,移除特定回调。once
:一次性订阅,事件触发后自动取消。
3. 在 Vue 3 中使用
我们可以在 Vue 3 项目中引入这个 EventBus
,并在组件中使用它。
示例:组件间通信
假设有两个组件 ComponentA
(发布者)和 ComponentB
(订阅者),它们通过事件总线通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- ComponentA.vue --> <template> <button @click="sendMessage">发送消息</button> </template> <script> import { defineComponent } from 'vue'; import eventBus from './eventBus'; export default defineComponent({ setup() { const sendMessage = () => { eventBus.emit('message', 'Hello from ComponentA!'); }; return { sendMessage }; } }); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<!-- ComponentB.vue --> <template> <div>收到消息: {{ message }}</div> </template> <script> import { defineComponent, ref, onMounted, onUnmounted } from 'vue'; import eventBus from './eventBus'; export default defineComponent({ setup() { const message = ref(''); // 订阅事件 const handleMessage = (msg) => { message.value = msg; }; onMounted(() => { eventBus.on('message', handleMessage); }); // 清理订阅,防止内存泄漏 onUnmounted(() => { eventBus.off('message', handleMessage); }); return { message }; } }); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- App.vue --> <template> <ComponentA /> <ComponentB /> </template> <script> import { defineComponent } from 'vue'; import ComponentA from './ComponentA.vue'; import ComponentB from './ComponentB.vue'; export default defineComponent({ components: { ComponentA, ComponentB } }); </script> |
输出:
- 点击
ComponentA
的按钮后,ComponentB
会显示:收到消息: Hello from ComponentA!
。
4. 结合 Composition API
为了更好地封装发布-订阅逻辑,我们可以使用 Composition API 创建一个可复用的 useEventBus
组合函数。
实现 useEventBus
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// useEventBus.js import { onMounted, onUnmounted } from 'vue'; import eventBus from './eventBus'; export function useEventBus() { const on = (eventName, callback) => { onMounted(() => eventBus.on(eventName, callback)); onUnmounted(() => eventBus.off(eventName, callback)); }; const emit = (eventName, ...args) => { eventBus.emit(eventName, ...args); }; const once = (eventName, callback) => { onMounted(() => eventBus.once(eventName, callback)); }; return { on, emit, once }; } |
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- ComponentA.vue --> <template> <button @click="sendMessage">发送消息</button> </template> <script> import { defineComponent } from 'vue'; import { useEventBus } from './useEventBus'; export default defineComponent({ setup() { const { emit } = useEventBus(); const sendMessage = () => { emit('message', 'Hello from ComponentA!'); }; return { sendMessage }; } }); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!-- ComponentB.vue --> <template> <div>收到消息: {{ message }}</div> </template> <script> import { defineComponent, ref } from 'vue'; import { useEventBus } from './useEventBus'; export default defineComponent({ setup() { const message = ref(''); const { on } = useEventBus(); on('message', (msg) => { message.value = msg; }); return { message }; } }); </script> |
优势:
- 自动清理:通过
onMounted
和onUnmounted
,确保组件销毁时自动取消订阅,避免内存泄漏。 - 可复用:
useEventBus
可以在多个组件中复用,减少重复代码。
5. 使用第三方库
如果你不想自己实现,可以使用现成的轻量级库:
mitt
:一个非常流行的微型事件发射器。- 安装:
npm install mitt
- 使用:
1 2 3 |
// eventBus.js import mitt from 'mitt'; export default mitt(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<!-- ComponentA.vue --> <script> import eventBus from './eventBus'; export default { setup() { const sendMessage = () => { eventBus.emit('message', 'Hello!'); }; return { sendMessage }; } }; </script> <!-- ComponentB.vue --> <script> import { ref, onMounted, onUnmounted } from 'vue'; import eventBus from './eventBus'; export default { setup() { const message = ref(''); onMounted(() => { eventBus.on('message', (msg) => (message.value = msg)); }); return { message }; } }; </script> |
6. 与插槽的关系
发布-订阅模式可以与 Vue 3 的插槽结合使用。例如,子组件通过插槽暴露数据,父组件通过事件总线触发更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<!-- Child.vue --> <template> <slot :data="data"></slot> </template> <script> import { defineComponent, ref } from 'vue'; import { useEventBus } from './useEventBus'; export default defineComponent({ setup() { const data = ref('Initial Data'); const { on } = useEventBus(); on('updateData', (newData) => { data.value = newData; }); return { data }; } }); </script> <!-- Parent.vue --> <template> <Child v-slot="{ data }"> <div>{{ data }}</div> <button @click="updateData">更新数据</button> </Child> </template> <script> import { defineComponent } from 'vue'; import { useEventBus } from './useEventBus'; import Child from './Child.vue'; export default defineComponent({ components: { Child }, setup() { const { emit } = useEventBus(); const updateData = () => { emit('updateData', 'Updated Data'); }; return { updateData }; } }); </script> |
7. 优缺点
优点:
- 解耦:组件间无需直接依赖。
- 灵活:适用于跨层级或动态组件通信。
- 简单:实现和使用都相对直观。
缺点:
- 调试困难:事件多了可能难以追踪来源。
- 官方不推荐全局使用:Vue 3 更推崇
props
、provide/inject
或状态管理。
8. 总结
Vue 3 中发布-订阅模式可以通过自定义 EventBus
或第三方库实现,结合 Composition API 可以更优雅地管理订阅和清理。它特别适合非父子组件通信,但在大型应用中应谨慎使用,避免过度依赖全局事件总线。