Skip to content

观察者模式vs发布订阅模式

引入——有趣的小案例

当我们加入一个发放各种优惠券的群时,我们进入了观察者模式的世界。这个群就像一个精彩的主题,而我们作为群成员,其实就是观察者。每天我们都收到一大堆群消息,有关衣服、零食等优惠券。

过段时间,我厌烦了这源源不断的群消息,于是我直接退群了。但我并不希望错过所有的优惠,因此与群主进行了私下的交流,我只对洗衣液的优惠券感兴趣,让群主以后有洗衣液的优惠券私发给我。

这样,从观察者模式进化到了发布订阅模式。现在,我只订阅了我真正关心的优惠券——洗衣液,过滤掉了其他不感兴趣的优惠券。群主成为了与这个丰富主题之间的中间人,只有当有洗衣液的优惠券时,才会私下发送给我。

发布订阅模式

含义

发布-订阅是软件架构中的一种消息范式,**消息的发布者不会将消息直接发送给特定的订阅者。**而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

订阅者把自己想要订阅的事件注册到事件中心,当发布者发布事件到事件中心,由事件中心统一调度订阅者注册到调度中心的处理代码。

一次订阅发布的流程

发布者->事件中心<=>订阅者

订阅者需要向事件中心订阅指定的事件 -> 发布者向事件中心发布指定事件内容 -> 事件中心通知订阅者 -> 订阅者收到消息(可能是多个订阅者)。

image-20230907214925361

代码

EventEmitter类中,

  • 构造函数初始化一个空对象this.events,用于存储事件名及它们的回调。

  • 定义on(eventName,callback),用于订阅事件。

    • 若事件名不存在(没有订阅者订阅过),则this.events[eventName] = [callback];
    //events的结构
    events:{
    	eventName:[f]
    }
    • 若事件名存在(有人订阅过,存在其他回调函数),则this.events[eventName].push(callback);
    //events的结构
    events:{
    	eventName:[f,f]//(一个事件可以有很多回调函数,相当于多个订阅者)
    }
  • 定义emit(eventName, ...args),用于发布事件。若事件名存在,则遍历与事件名相关联的回调函数数组,依次执行这些回调函数,并传递可选参数args给它们。

  • 定义off(eventName, callback),用于取消订阅事件。若事件名存在,则遍历与事件名相关联的回调函数数组,找到并移除某个回调函数callback

javascript
class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(eventName, callback) {
        if (this.events[eventName]) {
            this.events[eventName].push(callback);
        } else {
            this.events[eventName] = [callback];
        }
    }
    emit(eventName, ...args) {
        if (this.events[eventName]) {
            this.events[eventName].forEach((cb) => cb(...args));
        }
    }
    off(eventName,callback){
        if(this.events[eventName]){
            //过滤是把不相等的保留,剔除相等的
      		 this.events[eventName]=this.events[eventName].filter(cb=>cb!==callback)
            /* 另一种方法,找下标,直接删除下标对应的回调
            let index=this.events[eventName].findIndex(cb=>cb===callback)
                this.events[eventName].splice(index,1) */
        }
    }
}
javascript
//测试
const event = new EventEmitter();
// 定义事件,由发布者emit,订阅者on
const handleEvent1 = (...args) => {
    console.log('事件1:以下洗衣液打八折:',...args);
};
const handleEvent2 = (...args) => {
    console.log('事件2:以下纸巾打六折:',...args);
};

//订阅事件
event.on("event1", handleEvent1);
event.on("event2", handleEvent2);

//发布事件
event.emit("event1", '蓝月亮','威露士');//输出:事件1:以下洗衣液打八折:蓝月亮 威露士
event.emit("event2",'维达','清风');//输出:事件2:以下纸巾打六折:维达 清风

//取消订阅后,再次发布事件,订阅者不会再收到消息
event.off("event1", handleEvent1);
event.emit("event1", '立白');//输出:无
javascript
//实际可能的业务代码
let event = new EventEmitter();

//某个功能ok了
event.emit('success');

//不同的模块能根据ok的功能开始干活了
MessageCenter.fetch() {
  event.on('success', () => {
    console.log('更新消息中心');
  });
}
Order.update() {
  event.on('success', () => {
    console.log('更新订单信息');
  });
}
Checker.alert() {
  event.on('success', () => {
    console.log('通知管理员');
  });
}

观察者模式

含义

观察者模式是经典软件设计模式中的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当这个主题对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

一次流程

目标<=>观察者,观察者观察目标(监听目标)-> 目标发生变化-> 目标主动通知观察者。

观察者.jpg

​ 图示展示了目标发生变化-> 目标主动通知观察者该过程

代码

(1)主题/目标

  • 构造函数初始化一个空数组this.observerList,用于存储观察者列表。

  • 定义addObserver(observer),用于添加观察者。

  • 定义removeObserver(observer),遍历观察者数组,移除观察者observer

  • 定义notifyObservers(...args),遍历观察者数组,调用观察者的notified方法,通知所有观察者,并传递可选参数args给它们。

javascript
class Subject {
    constructor() {
        this.observerList = [];
    }
    addObserver(observer) {
        this.observerList.push(observer);
    }
    removeObserver(observer) {
        this.observerList = this.observerList.filter((ob) => ob !== observer);
    }
    notifyObservers(...args) {
        this.observerList.forEach((ob) => ob.notified(...args));
    }
}

(2)观察者

  • 构造函数接受两个参数,name标识观察者的名称,subject为观察者绑定的主题。
    • 如果观察者对象实例化时传递了 subject参数,则执行 subject.addObserver(this),表示观察者对象将自己注册到主题对象的观察者列表中。(第一批进群的人,群消息都能看到)
    • 如果没有传递 subject,则暂时不会执行注册操作。后续可通过subject.addObserver(某observer);来与主题对象建立联系。(后面进群的人,看不到历史消息)
  • 定义notified(...args),用于在主题的状态发生变化时接收通知,含可选参数args
javascript
class Observer {
    constructor(name,subject) {
        this.name = name;
        if(subject){
            subject.addObserver(this)
        }
    }
    notified(...args) {
        console.log(this.name, '收到消息', ...args);
    }
}

(3)测试

javascript
const subject = new Subject();
const observer1 = new Observer('小明',subject);
const observer2 = new Observer('小红');
subject.addObserver(observer2);
subject.notifyObservers('hello','world');//小明 收到消息 hello world 小红 收到消息 hello world
subject.removeObserver(observer2);
subject.notifyObservers('hello','again');//小明 收到消息 hello again

二者区别

发布订阅模式观察者模式
角色发布者、事件中心和订阅者目标和观察者
关系多对多一对多
耦合度完全解耦,订阅者和发布者没有直接关联,而是通过事件中心进行关联松耦合(有些观念说观察者是解耦,其实解耦的是业务代码,不是目标和观察者本身)
优点灵活,发布者和订阅者可以不在一处,只需引入事件中心简单明了
缺点使用不当会造成数据流混乱,代码不好维护;
订阅的事件越多,内存消耗越大
较为复杂,事件多时难以调试和定位错误
耦合,需同时引入目标和观察者才能达到响应式的效果
应用场景异步通信、vue的全局事件总线EventBus、跨应用模式(如消息中间件)Vue 的响应式、单个应用内部

当在代码中发现有watch、watcher、observe、observer、listen、listener、dispatch、trigger、emit、on、event、eventbus、EventEmitter这类单词出现的地方,很有可能是在使用观察者模式发布订阅的思想。不妨点进它的源码实现看看其他coder在实现观察者模式发布订阅时有哪些巧妙的细节!

参考资料

理解【观察者模式】和【发布订阅】的区别

订阅发布模式和观察者模式的区别

(知乎)观察者模式 vs 发布订阅模式