【知识梳理】观察者模式和发布订阅模式

178 阅读5分钟

写在前面


  写这篇文章的起因在于学习到了HTML5引入的新特性pushStatereplaceState,这两个方法可以在不刷新页面的情况下,改变浏览器地址栏里面的url。这也是 Vuexhistory模式底层使用的知识。但是,这两个方法有一个特点,那就是不会触发popstate事件。也就是说你在浏览器监听popstate事件是无法监听到这两个方法的调用的。那么我们就不能进行相应的逻辑处理:如加载js文件。

  这就是一个大问题。对于其他的history.back()history.forward()history.go()方法,它们的调用就是会触发popstate事件的。那么解决办法是什么呢?就是重写这两个方法,在重写的方法里面手动触发事件,执行相应的回调逻辑。

  等我在掘金上搜索相关的文章时,一篇博主的解答使用了发布-订阅的设计模式重写方法。我虽然之前听过这个设计模式,但是一直没有深入理解和使用。再加上前几天的面试题中,也有一道补全发布-订阅代码的题目,特此抓紧研究一下。随着我搜索🔍的进行,发现多篇文章指向观察者模式和发布-订阅模式不是一样,两者的宏观思想是一样的,但是具体实现还是有区别的。特此记录学习。

观察者模式


  观察网模式顾名思义更容易理解,有两个角色:一个主体对象(Subject)和多个观察者对象(Observer)。这个Subject对象维护了一个依赖列表,当任何状态发生改变时自动通知列表中的对象,这些对象也就是Observer。

  举个实际的例子,我想应聘某个大公司的技术岗位,但是目前这个公司没有hc了。但是我又很想进这个公司怎么办?我就跟hr小姐姐说,我很期待加入贵公司,如果岗位被放出来了,请务必通知我来投递简历。在这个应聘的实际例子中,hr就是Subject,我和其他的候选人就是Observer。当这个event发生(岗位hc被放出来),hr就notify记录下来的所有求职者岗位状态的改变。

观察者模式 整个流程如上图所示,我们可以看到观察者模式的两个特点:

  1. Subject和Observer是彼此知道对方的存在的,你知道应聘公司有hr,hr知道有这么多的求职者。
  2. Subject维护的依赖列表的组成是Observer,然后由Observer执行各自的更新逻辑。

观察者模式代码实践

class Subject {
    constructor() {
        // 时间戳作为id
        this.id = new Date();
        // 内部维护的依赖列表
        this.obs = [];
    }
    // 添加观察对象
    add(observer) {
        if (!this.obs.includes(observer)) {
            this.obs.push(observer);
        }
    }
    // 移除观察对象 
    remove(observer) {
        let obs = this.obs;
        
        for (let i = 0;i < obs.length;i++) {
            if (obs[i] === observer) {
                obs.splice(i, 1);
            }
        }
    }
    // 发布通知
    notify() {
        let obs = this.obs;
        
        for (let i = 0;i < obs.length;i++) {
            obs[i].update();
        }
    }
}
class Observer { 
    constructor(name) {
        this.name = name;
    }
    update() {
        console.log(this.name, '甜甜的恋爱轮到你啦!')
    }
}
// 实际执行
let hr = new Subject();
let Ming = new Observer('Ming');
let Xing = new Observer('Xing');

hr.add(Ming);
hr.add(Xing);
hr.notify(); 
// Ming 甜甜的恋爱轮到你啦!
// Xing 甜甜的恋爱轮到你啦!

发布-订阅模式


  下面说一下发布-订阅模式,它是在观察者模式的基础上进一步发展来的。在发布-订阅模式中,消息的发送方,叫做发布者(publisher),消息的接收者叫做订阅者,但是消息是不会直接发送给订阅者的,而是需要一个第三方组件—信息中介。换句话说,发布者和订阅者是不知道的彼此的存在的,是信息中介将发布者和订阅者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。

  举个实际的例子,邮局订报纸。我订阅了《故事会》,我爸订阅了《人民日报》,我妈订阅了《女人花》,并且分别留下了自己的联系电话。那么当《故事会》新一期出来了,会被发往当地的邮局,邮局会根据订阅《故事会》杂志的人员列表,通知我去拿杂志。这个时候我爸和我妈是不会接收到通知的,因为他们并没有订阅《故事会》。

发布-订阅模式 整个流程如上图所示,发布-订阅模式有两个特点:

  1. Publisher和Subscriber之间是不知道对方存在的。
  2. Publisher和Subscriber之间是存在一个信息中介,它过滤和筛选了实践,并根据主题事件,通知相应的Subscriber。

发布-订阅模式代码实践

class Publisher {
    constructor() {
        this.id = new Date();
        // 维护订阅中心
        this.subs = {};
    }
    // 订阅事件
    subscribe(event, fn) {
        let subs = this.subs;
        
        if (!subs[event]) {
            subs[event] = [];
        } 
        subs[event].push(fn);
    }
    // 取消订阅事件
    unSubscribe(event, fn) {
        let fns = this.subs[event];
        
        // 清除event事件下的所有回调
        if (!fn) {
            fns && fns.length = 0;
        } else {
        // 若传入的匿名函数fn,则无法清除
           for (let i = 0;i < fns.length;i++) {
               if (fns[i] === fn) {
                   fns.splice(i, 1);
               }
           }
        }
    }
    // 发布消息
    publish(event, ...args) {
        let fns = this.subs[event];
        
        if (!fns || fns.length <= 0) return;
        
        for (let i = 0; i < fns.length;i++) {
            fns[i].apply(this, args);
        }
    }
}
// 实际执行
let pub = new Publisher();
pub.subscribe('中彩票', function(money){
    console.log(`老子终于中了${money}啦,不用上班啦!`);
})
pub.subscribe('富婆', function(age) {
    console.log(`老子终于遇到我的${age}岁老宝贝了,不用奋斗啦!`);
})

pub.publish('中彩票', 五百万);
pub.publish('富婆', 53);
// 老子终于中了五百万啦,不用上班啦!
// 老子终于遇到我的53岁老宝贝了,不用奋斗啦!

总结


VS   观察者模式和发布-订阅模式都是定义了一个一对多的依赖关系,当有关状态发生变更时则执行相应的更新逻辑。不同的是,在观察者模式中依赖于Subject对象的一系列Observer对象在被通知之后只能执行同一个特定的更新方法,而在发布-订阅模式中则可以基于不同的主题去执行不同的自定义事件。相对而言,发布-订阅模式比观察者模式更加灵活多变。

参考文章


  1. Javascript中理解发布--订阅模式
  2. 观察者模式 vs 发布-订阅模式
  3. 谈谈观察者模式和发布订阅模式