发布-订阅模式

735 阅读3分钟

一、背景

在众多的设计模式里面,发布-订阅应该是最有名、最常见的了。

发布 - 订阅模式 (Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。

二、现实中的场景

场景1:

我们去菜鸟驿站取快递整个过程实际上就是一个发布订阅的过程。菜鸟驿站承包了这个小区的快递业务,就相当于我们这些收件人订阅了它,一旦有属于我们快递到了,就会收到菜鸟驿站给我们发布的短信提醒。

场景2:

平时关注的微信公众号推送也是一个发布订阅的过程:关注了该公众号,就是订阅了它,那么当公众号发布新内容的时候,就会给我们推送,这个就是发布的过程。

三、前端代码中的发布订阅

场景1: window.onload

window.addEventListener('load', function () {
	console.log('loaded!')
})

上面的代码再熟悉不过了,当页面内容全部加载完之后打印出loaded!。上面这个简单的代码实际上就是发布订阅模式。我们不知道页面什么时候能够加载完毕,所以订阅了windowload事件,当页面加载完毕之后,window就会像订阅者发布消息-即执行回调函数(打印出loaded!)。

场景2: vueEventBus

bus.js

import { Vue } from 'vue';
export const EventBus = new Vue();

订阅

A.vue

import { EventBus } from './bus';
EventBus.$on('myEvent', args => {
	console.log(args);
});

发布

B.vue

import { EventBus } from './bus';
EventBus.$emit('myEvent', 'hello world');

上面就是我们平时使用EventBus的姿势,$on方法是注册一个事件(即订阅),$emit则用来触发该事件(即发布)。


四、实现一个通用的发布-订阅模式

说了这么多,是时候手撸一个通用的发布-订阅模式了。

class Publisher {
    constructor() {
        this._subMap = {}  // 存放订阅者
    }

    // 订阅
    subscribe(type, cb) {
        if (this._subMap[type]) {
            if (!this._subMap[type].includes(cb)) {
                this._subMap[type].push(cb);
            }
        } else {
            this._subMap[type] = [cb];
        }
    }

    // 取消订阅
    unsubscribe(type, cb) {
        if (this._subMap[type] && this._subMap[type].includes(cb)) {
            const index = this._subMap[type].indexOf(cb);
            this._subMap[type].splice(index, 1);
        }
    }

    // 发布
    notify(type, args) {
        if (this._subMap[type]) {
            this._subMap[type].forEach(cb => cb(args))
        }
    }
};

/*
  验证demo
*/
const pub = new Publisher();

// 订阅
pub.subscribe('公众号1', fn1 = msg => console.log(`孙悟空订阅的公众号1: ${msg}`));
pub.subscribe('公众号1', fn2 = msg => console.log(`猪八戒订阅的公众号1: ${msg}`));
pub.subscribe('公众号2', fn3 = msg => console.log(`沙悟净订阅的公众号2: ${msg}`));
// 注意:要想取消订阅成功,必须用变量保存这个匿名函数,目的是为了用一个变量来固定指向该匿名函数的内存地址的指针。
pub.unsubscribe('公众号1', fn2);  

// 发布
pub.notify('公众号1', '今天我们的粉丝查过100万啦!!!');
pub.notify('公众号2', '明天给粉丝们寄礼物喽~~');

孙悟空订阅的公众号1: 今天我们的粉丝查过100万啦!!!
沙悟净订阅的公众号2: 明天给粉丝们寄礼物喽~~

上面就实现了一个通用的发布订阅模式,具备类型订阅、取消订阅功能,是不是很简单~~。 所以对于vueEventBus的订阅和发布功能也可以用代码简单实现一下:

class EventBus {
    constructor() {
        this.subMap = {};
    }
    // 订阅
    $on(name, cb) {
        if (this.subMap[name] && !this.subMap[name].includes(cb)) {
            this.subMap[name].push(cb);
        } else {
            this.subMap[name] = [cb];
        }
    }
    // 发布
    $emit(name, args) {
        if(this.subMap[name]) {
            this.subMap[name].forEach(cb => cb(args));
        }
    }
};

const eventBus = new EventBus();
eventBus.$on('sayHello', arg => console.log(`this is sayHello: ${arg}`));
eventBus.$on('goodBye', arg => console.log(`this is goodBye: ${arg}`));

eventBus.$emit('sayHello', 'mike');
eventBus.$emit('goodBye', 'mike');

// this is sayHello: mike
// this is goodBye: mike

五、发布-订阅模式的优缺点

优点

  1. 时间上的解耦 :注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知;
  2. 对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体。

缺点

  1. 增加消耗 :创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存;
  2. 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试。出现多对多的情况总是会很复杂~