给事件总线类型标注
这几天花了点时间写了个中国象棋对弈小网站, 今天在回顾代码的时候感觉现在用的事件总线太怪了, 一不小心就会写错.
ts
const ApiEvent = [
'API:UN_AUTH',
'API:NOT_FOUND',
'API:LOGOUT',
'API:FAIL',
'API:LOGIN',
] as const;
const ChessEvent = [
'MATCH:SUCCESS',
'GAME:START',
'GAME:END',
'CHESS:MOVE',
'CHESS:MOVE:END',
] as const;
type RequestCallback = (...args: any[]) => any;
type ResponseCallback = (...args: any[]) => any;
type Listener = (req: RequestCallback, resp: ResponseCallback) => void;
class EventEmitter<T extends readonly string[]> {
private eventNames: T;
listeners: Record<T[number], Set<Listener>> = {} as Record<
T[number],
Set<Listener>
>;
futureEvents: Record<
T[number],
{ req: RequestCallback; resp: ResponseCallback }[]
> = {} as Record<
T[number],
{ req: RequestCallback; resp: ResponseCallback }[]
>;
constructor(EventNames: T) {
this.eventNames = EventNames;
this.eventNames.forEach((eventName) => {
this.listeners[eventName as T[number]] = new Set<Listener>();
});
}
on(eventName: T[number], listener: Listener) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = new Set<Listener>();
}
this.listeners[eventName].add(listener);
if (this.futureEvents[eventName]) {
this.futureEvents[eventName].forEach(({ req, resp }) => {
listener(req, resp);
});
delete this.futureEvents[eventName];
}
}
off(eventName: T[number], listener: Listener) {
if (!this.listeners[eventName]) return;
this.listeners[eventName].delete(listener);
}
emit(
eventName: T[number],
req: RequestCallback = () => {},
resp: ResponseCallback = () => {}
) {
this.listeners[eventName].forEach((listener) => listener(req, resp));
}
futureEmit(
eventName: T[number],
req: RequestCallback = () => {},
resp: ResponseCallback = () => {}
) {
if (!this.futureEvents[eventName]) {
this.futureEvents[eventName] = [];
}
this.futureEvents[eventName].push({ req, resp });
}
}
const ApiBus = new EventEmitter(ApiEvent);
const GameBus = new EventEmitter(ChessEvent);
export { ApiBus, GameBus };
这里虽然用了 ts, 但是我为了传递信息和获取响应, 让 listener 接收两个参数:一个是 req, 另一个是 resp.
req 是一个 get 函数, 调用它获取数据; resp 是回调函数, 参数类型未知. 现在的问题就在于, req 和 resp 都是没有类型标注的, 只有刚开始写才知道自己在写什么, 也加大了以后的维护难度. 现在我写的代码里只有一个生产者, 但是如果以后再写一个生产者的话, 没有类型标注就得到处翻类型, 就会很痛苦.
这里 ApiBus 基本不涉及回调函数, 主要是 GameBus.
使用 Channel 代替 EventBus
在我的代码中, 我想的是 GameBus 主要用于 websocket 和棋盘的通信交互, 这里的未来事件也是用于 GameBus, 那在这里其实没有必要使用 EventBus, 而是采用 Channel.
Channel 与 EventBus 的异同
- 同
- 都有生产者和消费者
- 异
Channel 通常是一对一或者是多对一的关系(如果涉及并发操作, 也可以是多对多, 不过 js 是单线程), EventBus 一般是一对多.
Channel 在生产数据后, 如果没有对应的消费者来消费, 那么会将数据存起来而不是丢弃; EventBus 生产数据后一般直接广播而不是等待消费者来消费(未来事件除外)
js
class Channel {
constructor() {
this.eventsQueue = {};
this.listeners = {};
}
on(eventName, listener) {
this.listeners[eventName] = listener;
while (this.eventsQueue[eventName]?.length) {
const req = this.eventsQueue[eventName].shift();
if (req === undefined) {
return;
}
listener(req);
}
}
off(eventName) {
if (!this.listeners[eventName]) return;
delete this.listeners[eventName];
}
emit(eventName, req) {
if (!this.listeners[eventName]) {
this.eventsQueue[eventName].push(req);
return;
}
this.listeners[eventName](req);
}
}
export default new Channel();
简单用 js 写一下, 那接下来就是进行类型标注了. 虽然标题是给事件总线类型标注, 不过这里的方法是一样的.
首先定义每个事件以及对应的数据类型.
ts
type color = 'red' | 'black';
type position = { x: number; y: number };
interface GameEvents {
'MATCH:SUCCESS': null;
'GAME:START': {
color: color;
};
'GAME:END': {
winner: color;
};
'NET:GAME:START': {
color: color;
};
'NET:GAME:END': {
winner: color;
};
'NET:CHESS:MOVE': {
from: position;
to: position;
};
'NET:CHESS:MOVE:END': {
from: position;
to: position;
};
}
按实际调整. 接着给消息队列和 listeners 类型标注
ts
class Channel {
private eventsQueue: {
[K in keyof GameEvents]: Array<GameEvents[K]>;
};
private listeners: {
[K in keyof GameEvents]?: Listener<GameEvents[K]>;
} = {};
}
on, off, emit
ts
class Channel {
private eventsQueue: {
[K in keyof GameEvents]: Array<GameEvents[K]>;
};
private listeners: {
[K in keyof GameEvents]?: Listener<GameEvents[K]>;
} = {};
on<K extends keyof GameEvents>(
eventName: K,
listener: Listener<GameEvents[K]>
) {
if (!this.eventsQueue[eventName]) {
this.eventsQueue[eventName] = [];
}
this.listeners[eventName] = listener as Listener<
GameEvents[keyof GameEvents]
>;
while (this.eventsQueue[eventName]?.length) {
const req = this.eventsQueue[eventName].shift();
if (req === undefined) {
continue;
}
listener(req);
}
}
off(eventName: keyof GameEvents) {
if (!this.listeners[eventName]) return;
delete this.listeners[eventName];
}
emit<K extends keyof GameEvents>(eventName: K, req: GameEvents[K]) {
if (!this.listeners[eventName]) {
if (!this.eventsQueue[eventName]) {
this.eventsQueue[eventName] = [];
}
this.eventsQueue[eventName].push(req);
return;
}
this.listeners[eventName](req);
}
}
完整代码
ts
type color = 'red' | 'black';
type position = { x: number; y: number };
interface GameEvents {
'MATCH:SUCCESS': null;
'GAME:START': {
color: color;
};
'GAME:END': {
winner: color;
};
'NET:GAME:START': {
color: color;
};
'NET:GAME:END': {
winner: color;
};
'NET:CHESS:MOVE': {
from: position;
to: position;
};
'NET:CHESS:MOVE:END': {
from: position;
to: position;
};
}
type Listener<T> = (req: T) => void;
class Channel {
private eventsQueue: {
[K in keyof GameEvents]: Array<GameEvents[K]>;
} = {} as {
[K in keyof GameEvents]: Array<GameEvents[K]>;
};
private listeners: {
[K in keyof GameEvents]?: Listener<GameEvents[K]>;
} = {};
on<K extends keyof GameEvents>(
eventName: K,
listener: Listener<GameEvents[K]>
) {
if (!this.eventsQueue[eventName]) {
this.eventsQueue[eventName] = [];
}
this.listeners[eventName] = listener as Listener<
GameEvents[keyof GameEvents]
>;
while (this.eventsQueue[eventName]?.length) {
const req = this.eventsQueue[eventName].shift();
if (req === undefined) {
continue;
}
listener(req);
}
}
off(eventName: keyof GameEvents) {
if (!this.listeners[eventName]) return;
delete this.listeners[eventName];
}
emit<K extends keyof GameEvents>(eventName: K, req: GameEvents[K]) {
if (!this.listeners[eventName]) {
if (!this.eventsQueue[eventName]) {
this.eventsQueue[eventName] = [];
}
this.eventsQueue[eventName].push(req);
return;
}
this.listeners[eventName](req);
}
}
export default new Channel();
如果有需要的话, 也可以按照这种方法给EventBus类型标注.