在开发复杂的单页面应用时,我们经常会遇到一个问题:如何高效地在组件或模块之间进行通信?这里,EventBus(事件总线)就派上了用场。简单来说,EventBus 是一种设计模式,它允许不同组件或模块之间通过事件来通信,而无需直接引用彼此。
EventBus 是传统的组件通信解决方案,下面我们将讲解 EventBus 跨组件通信的原理、实现方式以及该如何使用。
原理解析EventBus 的核心在于提供一个中央机制,允许不同的组件或模块相互通信,而不必直接引用对方。它是一种典型的发布-订阅(pub-sub) 模式,这是一种广泛使用的设计模式,用于解耦发送者和接收者。
在这个模式中,EventBus 充当了一个中介的角色:它允许组件订阅那些它们感兴趣的事件,并在这些事件发生时接收通知。同样,当某个事件发生时,比如用户的一个动作或者数据的变化,EventBus 负责将这一消息广播给所有订阅了该事件的组件。
它基于三个核心操作:注册事件(on(event, callback))、触发事件(emit(event, ...args))、以及移除事件(off(event, callback))。因此,EventBus 的基本代码可以看到:
1 2 3 4 5 6 7 8 9 10 11 12 13 class EventBus { on (event, callback ) { } emit (event, ...args ) { } off (event, callback ) { } }
显然,我们需要有一个私有变量来储存用户的函数,此时为类添加 events 属性。events 属性是一个对象映射,其中每个属性表示一个事件名称,对应的值是一个回调函数的数组,这个数组存储了所有订阅了该事件的回调函数。
1 2 3 4 class EventBus { private events: Record<string , Function []> = {}; }
当用户执行订阅事件 on 时,回调函数会被添加到相应事件名称的数组中。这样,同一个事件可以被不同组件或模块订阅,而每个订阅者的回调函数都会被正确地保存在事件队列中。最后,当触发事件 emit 时,事件队列中的每个回调函数都会被执行,实现了事件的触发和通知功能。若已经没有订阅需求,则可以通过 off 移除已经订阅的事件。
代码实现接下来我们按照前文所述完善我们的代码实现:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 class EventBus { private events: Record<string , Function []> = {}; public on(eventName: string , callback : Function ): this { if (typeof callback !== "function" ) { throw new Error ("EventBus 'on' method expects a callback function." ); } if (!this .events[eventName]) { this .events[eventName] = []; } this .events[eventName].push(callback); return this ; } public emit(eventName: string , ...args: any []): this { const callbacks = this .events[eventName]; if (callbacks) { callbacks.forEach((callback ) => callback(...args)); } return this ; } public off(event?: string | string [], callback?: Function ): this { if (!event || (Array .isArray(event) && !event.length)) { this .events = {}; return this ; } if (Array .isArray(event)) { event.forEach((e ) => this .off(e, callback)); return this ; } if (!callback) { delete this .events[event]; return this ; } const callbacks = this .events[event]; if (callbacks) { const index = callbacks.indexOf(callback); if (index > -1 ) { callbacks.splice(index, 1 ); } } return this ; } }
当涉及到一次性的事件监听需求时,我们可以进一步扩展 EventBus,以支持一次性事件监听。允许用户在某个事件触发后,自动移除事件监听器,以确保回调函数只执行一次:
1 2 3 4 5 6 7 8 9 10 11 12 13 class EventBus { public once(eventName: string , callback : Function ): this { const onceWrapper = (...args: any [] ) => { this .off(eventName, onceWrapper); callback(...args); }; this .on(eventName, onceWrapper); return this ; } }
使用方式我们将类的封装到 event-bus.ts 中,通过模块的来管理:
1 2 3 export class EventBus { }
我们现在已经封装好了一个类,若我们像使用则需要实例化。此处再文件内直接实例化一个类:
1 2 export const eventBus = new EventBus();
这样使用时可以提供两种方式:
引入已经实例化的 eventBus
1 2 3 4 5 6 7 8 9 10 import { eventBus } from './event-bus' ;eventBus.on('eventName' , callback); eventBus.emit('eventName' , data); eventBus.off('eventName' , callback);
需要多个独立的事件总线实例时,或者希望在不同模块或组件之间使用不同的事件总线时,可以选择额外实例化 eventBus。这样做的目的可能是为了隔离命名的冲突、组件与模块逻辑隔离等原因。
1 2 3 4 5 6 import { EventBus } from './event-bus' ;export const eventBusA = new EventBus();export const eventBusB = new EventBus();
1 2 3 4 5 6 7 8 9 import {eventBusA, eventBusB} from './events' eventBusA.on('eventA' , callbackA); eventBusB.on('eventB' , callbackB); eventBusA.emit('eventA' , dataA); eventBusB.emit('eventB' , dataB);
以下是 CodeSandbox 的演示代码:
总结在本文中,我们深入探讨了 EventBus 的原理,了解了它是如何工作的。我们学习了它的核心操作。除了本文所提及的实现方式,有时候在生产项目中,为了确保代码的可靠性,我们可以考虑使用成熟的第三方库,例如 mitt 或 tiny-emitter 。
这些库已经经过广泛的测试和使用,可以提供稳定和可靠的 EventBus 功能。