TypeScript中替代extends实现EventBus注入并保留链式调用上下文的方案
对应设计范式
你判断的方向完全正确:EventBus和业务类是**has-a(有一个)**的组合关系,不符合is-a(是一个)的继承逻辑,这个场景的核心设计原则是组合优于继承(Composition Over Inheritance),不需要通过extends继承EventBus就能实现你要的链式调用、类型校验、自定义事件列表的能力。
方案1:依赖注入+方法转发(最符合DI原则)
核心思路是把EventBus实例作为业务类的私有依赖,不对外暴露整个EventBus实例,只把需要对外提供的on/off/once方法做一层转发,转发时返回业务类自身的实例上下文,保证链式调用时既能调用事件方法,也能调用业务类自身的方法。
首先给出修正后的可链式调用的EventBus基础实现:
type EventCallback = (...args: any[]) => void; type EventListeners = Record<string, EventCallback[]>; class EventBus<SupportedEvents extends string = string> { private eventListeners: EventListeners = {}; constructor(events: SupportedEvents[] = []) { events.forEach(eventName => { this.eventListeners[eventName] = []; }); } on<T extends SupportedEvents>(event: T, callback: EventCallback): this { if (!this.eventListeners[event]) this.eventListeners[event] = []; this.eventListeners[event].push(callback); return this; } off<T extends SupportedEvents>(event: T, callback?: EventCallback): this { if (!this.eventListeners[event]) return this; if (!callback) { this.eventListeners[event] = []; return this; } this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback); return this; } once<T extends SupportedEvents>(event: T, callback: EventCallback): this { const wrapper = (...args: any[]) => { callback(...args); this.off(event, wrapper); }; return this.on(event, wrapper); } protected emit<T extends SupportedEvents>(event: T, ...args: any[]): void { this.eventListeners[event]?.forEach(cb => cb(...args)); } }
业务类实现如下,不需要继承EventBus:
class Something { // 私有注入EventBus实例,泛型传入当前类支持的事件列表做类型校验 private eventBus: EventBus<'ready' | 'destroyed'>; constructor() { // 初始化时传入支持的事件列表 this.eventBus = new EventBus(['ready', 'destroyed']); } // 转发on方法,返回当前业务实例保证链式上下文正确 on( event: Parameters<EventBus<'ready' | 'destroyed'>['on']>[0], callback: Parameters<EventBus<'ready' | 'destroyed'>['on']>[1] ): this { this.eventBus.on(event, callback); return this; } off( event: Parameters<EventBus<'ready' | 'destroyed'>['off']>[0], callback?: Parameters<EventBus<'ready' | 'destroyed'>['off']>[1] ): this { this.eventBus.off(event, callback); return this; } once( event: Parameters<EventBus<'ready' | 'destroyed'>['once']>[0], callback: Parameters<EventBus<'ready' | 'destroyed'>['once']>[1] ): this { this.eventBus.once(event, callback); return this; } // 业务类自身方法,同样支持链式调用 doSomething() { console.log('run business logic'); // 内部触发事件 this.eventBus.emit('ready'); return this; } }
调用效果完全符合预期:
const someInstance = new Something(); someInstance .on('ready', () => console.log('component ready')) .doSomething() // 可以正常链式调用业务方法 .on('destroyed', () => console.log('component destroyed'));
如果传入不在支持列表里的事件名,TS会直接抛类型错误,和继承方案的类型体验一致。
方案2:类型安全Mixin(适合多类复用事件能力)
如果项目里有大量类需要复用EventBus能力,不想每个类都重复写方法转发逻辑,可以用TS的Mixin模式抽离复用逻辑,本质还是组合,不会产生EventBus作为父类的继承依赖:
// 定义构造函数类型约束 type Constructor<T = {}> = new (...args: any[]) => T; /** * 给任意类挂载EventBus能力的Mixin函数 * @param Base 要挂载能力的基类 * @param supportedEvents 类支持的事件列表 */ function WithEventBus<SupportedEvents extends string, TBase extends Constructor>( Base: TBase, supportedEvents: SupportedEvents[] ) { return class extends Base { private _eventBus = new EventBus<SupportedEvents>(supportedEvents); on = ((event: SupportedEvents, cb: EventCallback) => { this._eventBus.on(event, cb); return this; }) as EventBus<SupportedEvents>['on']; off = ((event: SupportedEvents, cb?: EventCallback) => { this._eventBus.off(event, cb); return this; }) as EventBus<SupportedEvents>['off']; once = ((event: SupportedEvents, cb: EventCallback) => { this._eventBus.once(event, cb); return this; }) as EventBus<SupportedEvents>['once']; protected emit = this._eventBus['emit'].bind(this._eventBus); }; }
业务类使用时不需要手动写转发逻辑:
class Something extends WithEventBus(class {}, ['ready', 'destroyed']) { doSomething() { console.log('run business logic'); this.emit('ready'); return this; } }
注意:这里的
extends不是继承EventBus,而是继承Mixin生成的包装类,事件能力以私有属性的形式组合在实例中,和直接依赖注入的逻辑完全一致,只是抽离了重复的转发代码。
方案选型参考
- 依赖注入+方法转发:依赖关系最清晰,无额外继承层级,调试时实例结构透明,适合事件能力仅在少数类使用的场景
- Mixin模式:代码复用率高,类型安全,适合大量类需要复用事件能力的场景,不会破坏原型链的可维护性
内容的提问来源于stack exchange,提问作者Thieu
相关产品推荐
相关产品推荐

