
发布订阅的实现相当简单,回调就是,但往往大家写的时候都不注意,随意糊弄,连类型安全都没有,尤其是 JS 项目中,大脑和代码是完全解耦的,所以我在某次项目中也自己写了个,想起了给发个包同时记录下心得。
常见的事件主线实现
Pub/Sub 最常见的实现是暴露出一个 EventBus 也就是事件主线,而后在这个主线上处理事件,但这样做就有两个坏处:
需要花费精力维护“事件”本身。通过事件主线发布订阅,势必就要通过“事件”这个东西来进行类型推导,比如说
export interface MyEvent<Payload, Result> extends String { }我们用字符串作为事件名,就需要定义一个类型和一个被声明为某种事件的字符串变量,如果我们将发布订阅当做应用的核心机制,这些字符串变量将会迅速膨胀,然后就有了数十上百个奇怪的字符串变量,这是没有必要的。
过于复杂的API。在基于事件主线的发布订阅中,我们定义一个行为需要用到多少API呢?
定义一个字符串变量作为事件,全局变量或局部变量
export const event1 = "event1" as MyEvent<string, string>从事件主线上执行 发布/订阅/退订 操作,
useEffect(() => {} const callback = (s: string) => s; EventBus.subscribe(event1, callback); return EventBus.unsubscribe(event1, callback) , [])
用到了预先定义的 EventBus、MyEvent 和自行定义的 event1,同时需要储存 callback 变量,不算太复杂,但我们可以更简单。
充分利用 JS 的第一等函数简化API
JS 的一个好处是函数作为一等公民被支持,我们完全可以将事件主线的 unsubscribe 作为 subscribe 的返回值,如此则简化了取消订阅的API,同时,因为闭包的存在 callback 将不再需要储存。
再看这个碍事的事件名,取消订阅不再需要他了,剩下的就是发布和订阅了,我们完全也可以通过闭包将事件名封起来,只需要返回发布订阅两个函数就是了。
再考虑下异步支持和单播多播的问题,我们就得到了这个定义,没有事件主线也没有事件。
type MaybePromise<T> = T | Promise<T>;
type Handler<Payload = any, Result = any> = (payload: Payload) => MaybePromise<Result>;
export type Sender<P = any, R = any> = {
subscribe: (handler: Handler<P, R>) => () => void;
publish: (payload: P) => MaybePromise<R> | void;
};
export type Notifier<P = any> = {
subscribe: (handler: Handler<P, void>) => () => void;
publish: (payload: P) => MaybePromise<void>
};接下来是具体的实现,在底层我们仍然需要储存一个字符串和回调的映射表,但就没必要定义 class 了,这是 JS 的世界那玩意不必要。
const registry = new Map<string, Handler | Handler[]>();
const subscribe = <Payload, Result>(cmd: string, handler: Handler<Payload, Result>, multicast = false) => {
multicast ? registry.set(cmd, [...((registry.get(cmd) as Handler[]) || []), handler]) : registry.set(cmd, handler);
return () => {
multicast ? (registry.get(cmd) as Handler[]).splice((registry.get(cmd) as Handler[]).indexOf(handler), 1) : registry.delete(cmd);
};
};至于单播多播两种类型其实只是在映射表的订阅上包了一层,到这里,我们可以数一下我们现在执行一个发布订阅操作需要多少API,一个而已:创建一个单播组或者多播组。拢共不到一百行,SO EASY !
export function createSender<Payload, Result>(cmd: string): Sender<Payload, Result> {
return {
subscribe: (handler: Handler<Payload, Result>) => subscribe(cmd, handler, false),
publish: (payload: Payload) => {
return (registry.get(cmd) as Handler<Payload, Result>)?.(payload);
},
};
}
export function createNotifier<Payload>(cmd: string): Notifier<Payload> {
return {
subscribe: (handler: Handler<Payload, void>) => subscribe(cmd, handler, true),
publish: async (payload: Payload) => {
const results = (registry.get(cmd) as Handler<Payload, void>[] | undefined)?.map((h) => h(payload));
results && (await Promise.allSettled(results));
},
};
}