设计模式之发布订阅与观察者

1,788 阅读3分钟

1、发布订阅模式

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

 ╭─────────────╮                 ╭───────────────╮   Fire Event   ╭──────────────╮
 │             │  Publish Event  │               │───────────────>│              │
 │  Publisher  │────────────────>│ Event Channel │                │  Subscriber  │
 │             │                 │               │<───────────────│              │
 ╰─────────────╯                 ╰───────────────╯    Subscribe   ╰──────────────╯
​

实现思路

  • 创建一个对象
  • 在该对象上创建一个缓存列表(调度中心)
  • on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
  • emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
  • off 方法可以根据 event 值取消订阅(取消订阅)
  • once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
class EventEmitter {
​
  constructor(){
    // 维护事件及订阅行为 缓存列表(调度中心)
    this.events = {}
  }
​
​
  /**
         * 注册事件监听者 (订阅者)
         * @param {String} type 事件类型
         * @param {Function} cb 回调函数
         */
  on(type,cb){
    // 判断是否已存在 不存在创建事件类型
    if(!this.events[type]){
      this.events[type] = []
    }
    this.events[type].push(cb);
  }
​
  /**
         * 注册一次 调用后自动删除
         * @param {String} type 事件类型
         * @param {Function} cb 回调函数
         */
  once(type,cb){
    const _this = this;
    function on () {
      _this.off(type, on);
      cb.apply(_this, arguments);
    }
    this.on(type, on);
  }
​
  /**
         * 发布事件 (发布者)
         * @param {String} type 事件类型
         * @param {...any} args 参数列表,把emit传递的参数赋给回调函数
         */
  emit(type, ...args){
    // 循环触发已注册的事件
    if(this.events[type]){
      this.events[type].forEach(element => {
        element(...args);
      });
    }
  }
​
  /**
         * 取消事件
         * @param {String} type 事件类型
         * @param Function} cb 回调函数
         */
  off(type,cb){
    const evenItemData = this.events[type];
    if(evenItemData){
      // 判断是否存在相同的事件 存在则移除
      evenItemData.forEach((item,i)=>{
        if(item === cb){
          evenItemData.splice(i, 1);
        }
      })
      // 如果没有内容则清楚对象
      if(this.events[type].length === 0 ){
        delete this.events[type];
      }
    }
  }
​
}
​
​
const eventEmitter = new EventEmitter();
​
const aFun = (a)=>{
  console.log("我是click方法一"+a)
}
​
​
const bFun = (b)=>{
  console.log("我是click方法二"+b)
}
​
​
const cFun = ()=>{
  console.log("我是hhhh 注册一次")
}
​
eventEmitter.on("click",aFun)
​
eventEmitter.on("click",bFun)
​
​
eventEmitter.once("hhhh",cFun)
​
eventEmitter.emit("click","aaaa");
eventEmitter.emit("hhhh");
​
​
eventEmitter.off("click",bFun)
​
​
eventEmitter.emit("click","bbbb"); 
​
eventEmitter.emit("hhhh"); // 已注销 订阅不到

总结

优点
  • 对象之间解耦
  • 异步编程中,可以更松耦合的代码编写
  • 易理解,可类比于DOM事件中的dispatchEventaddEventListener
缺点
  • 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱
  • 创建订阅者本身要消耗一定的时间和内存

2、观察者模式

观察者模式与发布订阅模式相比,耦合度更高,通常用来实现一些响应式的效果。在观察者模式中,只有两个主体,分别是目标对象Subject,观察者Observer

  • 观察者需Observer要实现update方法,供目标对象调用。update方法中可以执行自定义的业务代码。
  • 目标对象Subject也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。Subject需要维护自身的观察者数组observerList,当自身发生变化时,通过调用自身的notify方法,依次通知每一个观察者执行update方法。
 ╭─────────────╮  Fire Event  ╭──────────────╮
 │             │─────────────>│              │
 │   Subject   │              │   Observer   │
 │             │<─────────────│              │
 ╰─────────────╯  Subscribe   ╰──────────────╯
​
​
​
    // 观察者
    class Observer {
      /**
       * 构造器
       * @param {Function} cb 回调函数,收到目标对象通知时执行
       */
      constructor(cb) {
        if (typeof cb === 'function') {
          this.cb = cb
        } else {
          throw new Error('Observer构造器必须传入函数类型!')
        }
      }
      /**
       * 被目标对象通知时执行
       */
      update() {
        this.cb()
      }
    }
​
    // 目标对象
    class Subject {
      constructor() {
        // 维护观察者列表
        this.observerList = []
      }
      /**
       * 添加一个观察者
       * @param {Observer} observer Observer实例
       */
      addObserver(observer) {
        this.observerList.push(observer)
      }
      /**
       * 通知所有的观察者
       */
      notify() {
        this.observerList.forEach(observer => {
          observer.update()
        })
      }
    }
​
    const observerCallback = function () {
      console.log('我被通知了')
    }
    const observer = new Observer(observerCallback)
​
    const observer2 = new Observer(observerCallback)
​
    const subject = new Subject();
    subject.addObserver(observer);
    subject.addObserver(observer2);
    subject.notify();
​

总结

  • 角色很明确,没有事件调度中心作为中间者,目标对象Subject和观察者Observer都要实现约定的成员方法。
  • 双方联系更紧密,目标对象的主动性很强,自己收集和维护观察者,并在状态变化时主动通知观察者更新。

差异

  • 在观察者模式中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
  • 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
  • 观察者模式大多数时候是同步的,比如当事件触发,Subject 就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
  • 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

个人认为,观察者模式和发布订阅模式本质上的思想是一样的,而发布订阅模式可以被看作是观察者模式的一个进阶版。