TS 设计模式08 - 发布订阅模式

1. 简介

前面介绍了观察者模式,就好比我们去点餐,通知服务员说,餐好了跟我说一下。那么服务员和顾客之间就形成了耦合,首先服务员得知道餐品好了以后通知那些顾客,其次,如果是多位服务员协作,每个服务员都需要知道这些顾客。

但事实上你发现去 kfc 点餐的时候,服务员并没有直接通知我们。而是采用叫号的方式。细想一下,你去 kfc,是不是可以在点餐系统进行排号(网上或者排队,这里抽象一下),餐品好了以后,服务员输入点餐号,点一下完成即可,点餐系统会通知对应的顾客取餐。

这里你和服务员之间的消息通过点餐系统来传递,你并不需要知道是谁点的完成,服务员也不需要知道这份餐品给谁。完美解耦了消息的发送者和接收者。更好地是,我们在点餐或者叫号的时候其实还可以指定行为,比如说 66 号产品好了以后帮我送到 A1 桌。

再比如说炒股的时候,我们可以委托挂单,就是当股票到了某一个价格就帮你买入或者卖出,等等,例子很多。

2. 用例图

image.png

3. 实现

node 中 EventEmitter 就是这样一个典型例子。我们来简单实现一个 EventEmitter。

interface HandlerInfo {
    handler: Function;
    once?: boolean;
}
class EventEmitter {
    private events: Map = new Map();
    on(type: string, handler: Function, once?: boolean) {
        if (!this.events.has(type)) {
            this.events.set(type, []);
        }
        (this.events.get(type) || []).push({
            handler,
            once,
        });
        return () => {
            this.off(type, handler);
        };
    }
    once(type: string, handler: Function) {
        return this.on(type, handler, true);
    }
    emit(type: string, ...args) {
        let i = 0;
        while (i < (this.events.get(type) || []).length) { // 这里每次都从 this.events 去动态读取,方中途被变更
            const handlers: HandlerInfo[] = this.events.get(type) || [];
            const { handler, once } = handlers[i];
            // 如果是一次性的,应该在调用前删除,防止这里会自己触发自己,导致无限循环或者次序错乱
            if (once) {
                handlers.splice(i--, 1);
            }
            i++;
            handler(...args); // 这里 this 就交给传入的 handler 来保证了
        }
    }
    off(type?: string, handler?: Function): void {
        if (!type) return; // 最好不要默认全部清除,不安全
        if (!handler) {
            this.events.set(type, []); // 因为这里是直接赋值清空,所以在 emit 的时候,记得每次都从 events 动态获取
            return;
        }
        this.events.set(type, (this.events.get(type) || []).filter(item => item.handler !== handler));
    }
}

const eventEmitter = new EventEmitter();

class Person {
    public name: string;
    constructor(name: string) {
        this.name = name;
    }
    weatherSubscribe(once?: boolean): Function {
        return eventEmitter.on('weather', (weather) => {
            switch (weather) {
                case '雨':
                    console.log(`${this.name}在家看电影`);
                    break;
                default:
                    console.log(`${this.name}出去玩`);
            }
        }, once);
    }
    weatherNotify(weather) {
        eventEmitter.emit('weather', weather);
    }
}

const xiaoWang = new Person('小王');
const xiaoMing = new Person('小明');
const xiaoZhang = new Person('小张');

xiaoWang.weatherSubscribe(true);
const off = xiaoMing.weatherSubscribe();

xiaoZhang.weatherNotify('雨');
xiaoZhang.weatherNotify('晴');

off();
xiaoZhang.weatherNotify('晴');
image.png

4. 小结

发布订阅模式可以说是对观察者模式的进一步抽象。

我们通过消息中心对消息进行统一处理,那么这里通知者和消费者的关系其实被弱化了,它们可以是任意对象,通知者和消费者也可以是同一个对象,这种模式甚至在非对象也可以使用,即我们只关注发布和订阅行为本身,而不关心发布订阅者是谁。

参考

从发布订阅模式入手读懂Node.js的EventEmitter源码
使用typescript 写一个简单的事件监听/发布订阅模式的类
TypeScript 设计模式之发布-订阅模式
观察者模式和发布订阅模式的区别
图解23种设计模式(TypeScript版)——前端必修内功心法
观察者模式 vs 发布订阅模式
设计模式之发布订阅模式(1) 一文搞懂发布订阅模式
github - node/lib/events
github - wxpage/lib/message
Node中EventEmitter理解与简单实现

你可能感兴趣的:(TS 设计模式08 - 发布订阅模式)