设计模式-发布订阅

70 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

发布订阅,又叫做观察者模式,定义对象间的一(发布者)对多(订阅者)的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知

如果使用过Vue,你可能也使用过它的EventBus进行组件数据通信,而这也正是发布订阅模式的一个很典型的例子。

先看一个简单发布订阅模式的实现:

const eventBus = {
    eventList: {},
    /** 订阅 */
    $on(event, fn) {
        if (!this.eventList[event]) {
            this.eventList[event] = [];
        }
        this.eventList[event].push(fn);
    },
    /** 发布 */
    $emit() {
        // 利用数组shift取出发布事件的key
        let key = Array.prototype.shift.call(arguments),
            // fns, 找到订阅数组
            fns = this.eventList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (let i = 0; i < fns.length; i++) {
            let fn = fns[i];
            fn.apply(this, arguments);
        }
    },
};

eventBus.$on('saysay', (name) => {
    console.log(name);
});

eventBus.$on('saysay', (name) => {
    console.log(name, '2333');
});

eventBus.$emit('saysay', 'jimous');

首先我们内部维护了一个事件队列(Object类型),通过$on方法进行事件注册,该方法接收两个参数,一个是用来定义事件的key,一个是该事件对应的执行函数, 同时将key和对应执行函数添加到事件队列中;$emit方法利用函数的argument隐性参数,通过获取第一个参数,即事件的key,然后从事件队列中取出对应key下的执行函数数组,然后遍历调用执行函数,并将剩余参数传入。

基于类的实现,对不同业务侧实现实例隔离

有一些业务场景中,单纯的事件区分业务可能不能满足需要,不同的业务模块,维护各自不同的发布订阅实例会是更好的选择,那么如何实现呢,首先我们基于类去实现发布订阅,这里单例模式思想有了用武之地了

  • 单例模式实现模块隔离
class MessageEvent {
    static commonData;
    static instance = {};
    /**业务侧调用eventInit方法,将type传入进行模块隔离并将每个模块进行单例处理 */
    static eventInit(type) {
        // 共享MessageEvent.commonData
        if (!this.instance[type]) {
            this.instance[type] = new MessageEvent();
        }
        return this.instance[type];
    }
    constructor() {
        this.eventList = [];
    }
    $on(event, fn) {
        if (!this.eventList[event]) {
            this.eventList[event] = [];
        }
        this.eventList[event].push(fn);
    }
    $emit() {
        let key = Array.prototype.shift.call(arguments),
            fns = this.eventList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (let i = 0; i < fns.length; i++) {
            let fn = fns[i];
            fn.apply(this, arguments);
        }
    }
}

export const eventBusA = MessageEvent.eventInit('A');
export const eventBusB = MessageEvent.eventInit('B');

每块业务对应各自的单例,保证了我们对每块业务的eventbus对象的引用,都是指向同一个实例,各自隔离,同时在创建单例之前,共享一个MessageEvent父级对象,因此我们可以扩展其他static属性,作为公共数据初始化使用,比如commonData

  • 更加简单粗暴,直接抛出实例化对象

如果不需要额外维护其他初始化变量,那么以下方式则更加直接方便。

class MessageEvent {
    constructor() {
        this.eventList = [];
    }
    $on(event, fn) {
        if (!this.eventList[event]) {
            this.eventList[event] = [];
        }
        this.eventList[event].push(fn);
    }
    $emit() {
        let key = Array.prototype.shift.call(arguments),
            fns = this.eventList[key];
        if (!fns || fns.length === 0) {
            return false;
        }
        for (let i = 0; i < fns.length; i++) {
            let fn = fns[i];
            fn.apply(this, arguments);
        }
    }
}

export const eventBusA = new MessageEvent();
export const eventBusB = new MessageEvent();

使用场景

  1. 异步编程中,可以用发布订阅模式作为回调函数的一种 替代方案;
  2. 可以取代对象间硬编码的通知机制,一个对象不用显式调用另一个对象的某个接口,可以直接通过发布订阅方式建立调用关系;
  3. 模块间数据通信,比如我们封装某个比较大的模块,模块内部不同的功能之间存在耦合,在拆分功能代码的时候,将耦合的部分可以通过发布订阅模式进行通信连接,实现解耦。
避免已不存在的事件订阅,当订阅消息未被触发,该订阅代码也一直在内存中,占用内存消耗;

同时要注意如果过度使用eventBus,对象之间的必要联系将被深埋在背后,导致程序难以跟踪维护和理解.(笔者曾经维护过的直播业务,里面使用了大量的eventBus来做直播场景下的前端数据通信,比如直播状态变更,播放器事件,用户交互,以及im通知,深受诟病。)

发布订阅与生产消费

简单来说,生产消费模式一定存在生产对应的产品属性,即有创建产品的对象实例;而发布订阅作为一种消息分发机制,并不需要该产品属性,只负责消息通信过程,所以从这个角度区分这两种模式的话,比较好理解。