You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 09:51:25