# EventBus 类型定义

这几天我在想，EventBus 这种事件总线机制我经常用到，因为他的写法形式很固定，我让 copilot、AmazonQ、千问给我补全，但大模型补全出来的类型都是 `any`，要么就是 `Function`，比如这样：

```typescript
export class EventBus {
  private listeners: Map<string, Function[]> = new Map();

  on(event: string, listener: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)?.push(listener);
  }

  off(event: string, listener: Function) {
    if (!this.listeners.has(event)) {
      return;
    }
    const fnList = this.listeners.get(event) || []
    const index = fnList.indexOf(listener);
    if (index !== -1) {
      this.listeners.set(event, fnList.slice(index, 1));
    }
  }

  clear(event: string) {
    this.listeners.delete(event);
  }

  emit(event: string, ...args: any[]) {
    if (!this.listeners.has(event)) {
      return;
    }
    const fnList = this.listeners.get(event) || []
    fnList.forEach((fn) => {
      fn.apply(this, args);
    });
  }
}
```

你说他不对吧，他 EventBus 该有的都有，做法也是没错的，但你说他正确吧，他这写的和 JavaScript 有啥区别呢？丢了类型补全，还存在类型安全隐患。

于是我学习了一下别人的做法：

```typescript
/* 基础事件 */
export interface IBaseEvent {
  id: string;
  type: EventType;
  timestamp: Date;
}

export interface IMenusLoad extends IBaseEvent {
  menu: Menu;
}

export interface IThemeChange extends IBaseEvent {
  theme: "light" | "dark";
}

export interface Events {
  "menus-load": IMenusLoad;
  "theme-change": IThemeChange;
}
```

首先定义**基础事件数据**接口 `IBaseEvent`，描述 `emit` 传递和 `listener` 接收的**事件数据**，里面的属性是一个事件数据必须有的。

`IMenusLoad` 和 `IThemeChange` 作为更具体的事件数据接口，他会带有自己事件相关的属性。

`Events` 是**事件类型**到**事件数据**的**映射**，也就是触发某个类型的事件时，所携带的数据的类型。

```typescript
export type T = keyof Events;

export type Listener<EventType extends T> = (e: Events[EventType]) => void;
```

T 是 Events 键组成的联合类型，通过 T 可以描述出 Events **所有事件类型对应的 listener**：`(e: Events[EventType]) => void`。

接着他做了一个观察者，为每个事件单独做观察监听，也就是说**一个事件类型，对应一个**`Observer` 实例：

```typescript
export interface IObserver<EventType extends T> {
  subscribe: (listener: Listener<EventType>) => () => void;
  publish: (event: Events[EventType]) => void;
}

export class Observer<EventType extends T> implements IObserver<EventType> {
  private listeners: Listener<EventType>[] = [];

  subscribe(listener: Listener<EventType>): () => void {
    this.listeners = [...this.listeners, listener];

    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  }

  publish(event: Events[EventType]): void {
    this.listeners.forEach((listener) => listener(event));
  }
}
```

以往我的习惯是一个对象来保存 `type` 到 `listeners` 的关系。

接着是 EventBus 的实现：

```typescript
type T = keyof Events;

type ObserversMap = {
  [Type in T]?: IObserver<Type>;
};

/* Singleton implementation of Global Event Bus */
class EventBus {
  private static instance: EventBus;
  private observers: ObserversMap = {};

  private constructor() {}

  static getInstance() {
    if (!EventBus.instance) {
      EventBus.instance = new EventBus();
    }

    return EventBus.instance;
  }

  public publish<Type extends T>(type: Type, event: Events[Type]): void {
    if (!EventBus.getInstance().observers[type]) {
      EventBus.getInstance().observers = {
        ...EventBus.getInstance().observers,
        [type]: new Observer<Type>(),
      };
    }

    // @ts-ignore
    EventBus.getInstance().observers[type].publish(event);
  }

  public subscribe<Type extends T>(
    type: Type,
    listener: Listener<Type>
  ): () => void {
    if (!EventBus.getInstance().observers[type]) {
      EventBus.getInstance().observers = {
        ...EventBus.getInstance().observers,
        [type]: new Observer<Type>(),
      };
    }

    // @ts-ignore
    return EventBus.getInstance().observers[type].subscribe(listener);
  }
}

export default EventBus.getInstance();
```

由于他是用**事件对应独立的**`Observers`，所以会用 `Map` 来管理映射关系，`ObserversMap` 是映射的类型定义，键来自于先前的 `Events`。

**私有的构造函数**和 `getInstance` 让 EventBus 实现了**单例模式**，项目中只会有一个 EventBus 对象存在。

`publish` 和 `subscribe` 的预检查逻辑一致，都是先判断 observers 中有没有某事件类型，没有就创建新的，然后订阅/发布。（这两个 ts-ignore 可以不用管，之前已经判断过对象存在了）

到这里 EventBus 的功能已经基本实现了。

接下来的是事件类：

```typescript
class BaseEvent implements IBaseEvent {
  public id: string;
  public timestamp: Date;

  constructor(public type: EventType) {
    this.id = generateKey();
    this.timestamp = new Date();
  }
}

export class MenusLoadEvent extends BaseEvent implements IMenusLoad {
  constructor(public menu: Menu) {
    super("menus-load");
  }
}

export class ThemeChangeEvent extends BaseEvent implements IThemeChange {
  constructor(public theme: "light" | "dark") {
    super("theme-change");
  }
}
```

`id` 和 `timestamp` 都是自动生成的，具体事件的参数在之前的 IBaseEvent 那已经定义了，这里是实现。

具体使用起来是这样的：

```typescript
EventBus.getInstance().subscribe("theme-change", (event) => console.log(event));
```

事件类型不能乱填，TS 会检查 `Events` 映射表的键，同理 `event` 的类型也会根据 Events 的值确定。

# 参考

[https://medium.com/@nijatismayilbeyli/implementing-type-safe-and-generic-event-bus-in-typescript-752ba94984ec](https://medium.com/@nijatismayilbeyli/implementing-type-safe-and-generic-event-bus-in-typescript-752ba94984ec)
