谈谈发布-订阅模式

3,651 阅读7分钟

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

场景

发布-订阅对于前端开发来讲是对比较常见的。以下的几种场景其实都是使用了发布-订阅模式。

  • DOM操作中的addEventListener
  • Vue中的事件总线的概念。
  • Node.js中的EventEmitter以及内置库。

什么是发布-订阅模式

发布-订阅是对象中的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。

举个例子,假如你在某平台订阅了某个专题,该系统会自动在该专题更新的时候,主动推送信息给到你,而不用你手动地去查阅。这个例子就类似发布-订阅模式

image.png

上图有几个概念需要理清

发布者:通过事件中心派发事件

订阅者:通过事件中心进行事件的订阅

事件中心:负责存放事件和订阅者的关系.

如何实现一个发布-订阅模式

基础版本

思路

  • 我们的事件中心我们使用Map的结构来进行存储。Map中,以事件名作为,而则是存放对应事件的订阅者.
  • on方法是事件订阅的方法,可以根据type来找到对应的订阅者的集合,我们将传入的函数callback添加到集合去。
  • emit方法是事件发布的方法,根据type来找到对应的订阅者的集合,并依次调用订阅者。
  • off方法是取消订阅的方法,根据type来找到对应的订阅者的集合,实质上是根据引用来匹配对应的订阅者,将其移出集合中。 代码如下:
class EventEmmiter {
  subscribes: Map<string, Array<Function>>;

  constructor() {
    this.subscribes = new Map();
  }

  /**
   * 事件订阅
   * @param type 订阅的事件名称
   * @param callback 触发的回调函数
   */
  on(type: string, callback: Function) {
    const sub = this.subscribes.get(type) || [];
    sub.push(callback);
    this.subscribes.set(type, sub);
  }

  /**
   * 发布事件
   * @param type 发布的事件名称
   * @param args 发布事件的额外参数
   */
  emit(type: string, ...args: Array<any>) {
    const sub = this.subscribes.get(type) || [];
    const context = this;
    sub.forEach(fn => {
      fn.call(context, ...args);
    })
  }

  /**
   * 取消订阅
   * @param type 取消订阅的事件名称
   * @param callback 取消订阅的具体事件
   */
  off(type: string, callback: Function) {
    const sub = this.subscribes.get(type);
    
    if(sub) {
      const newSub = sub.filter(fn => fn !== callback);
      this.subscribes.set(type, newSub);
    }
  }
}

const eventEmmiter = new EventEmmiter();

eventEmmiter.on('eventName', () => {
  console.log('第一次订阅');
});
eventEmmiter.emit('eventName');

const secondEmmiter = (a: number, b: number) => {
  console.log(`第二次订阅, 结果为${a + b}`);
}
eventEmmiter.on('eventName', secondEmmiter);
eventEmmiter.emit('eventName', 1, 3);

eventEmmiter.off('eventName', secondEmmiter);
eventEmmiter.emit('eventName', 1, 3);

输出结果

第一次订阅
第一次订阅
第二次订阅, 结果为4
第一次订阅

上述结果符合预期,这时,我们一个比较简易版的发布-订阅完成啦~

支持once订阅

我们使用过发布-订阅都应该知道发布-订阅直接一次订阅,即这个订阅者只会被通知一次。下面,我们来增加这个操作。

思路:我们的订阅者需要具有once的属性,来进行判断,并且,在每一次emit后,我们需要把once事件清除。

基于以上的思路,我们需要改变我们订阅者的数据结构。

// 此前的订阅者 (实质上就是函数)
subscribes: Map<string, Array<Function>>;

// 现在的订阅者
interface SubscribeEvent {
  fn: Function;
  once: boolean;
}

subscribes: Map<string, Array<SubscribeEvent>>;

同时,我们发现onceon的逻辑只在于参数的不同,这一部分进行抽离。所以我们有了以下的代码

interface SubscribeEvent {
  fn: Function;
  once: boolean;
}

class EventEmmiter {
  subscribes: Map<string, Array<SubscribeEvent>>;

  constructor() {
    this.subscribes = new Map();
  }

  addEvent(type: string, callback: Function, once: boolean = false) {
    const sub = this.subscribes.get(type) || [];
    sub.push({ fn: callback, once });
    this.subscribes.set(type, sub);
  }

  on(type: string, callback: Function) {
    this.addEvent(type, callback);
  }

  emit(type: string, ...args: Array<any>) {
    const sub = this.subscribes.get(type) || [];
    const context = this;
    
    sub.forEach(({ fn }) => {
      fn.call(context, ...args);
    });

    const newSub = sub.filter(item => !item.once);
    this.subscribes.set(type, newSub);
  }

  off(type: string, callback: Function) {
    const sub = this.subscribes.get(type);
    
    if(sub) {
      const newSub = sub.filter(({ fn }) => fn !== callback);
      this.subscribes.set(type, newSub);
    }
  }

  once(type: string, callback: Function) {
    this.addEvent(type, callback, true);
  }
}

const eventEmmiter = new EventEmmiter();
eventEmmiter.on('eventName', () => {
  console.log('第一次订阅');
});

eventEmmiter.once('eventName', () => {
  console.log('once test')
});


eventEmmiter.emit('eventName');

eventEmmiter.emit('eventName');

输出的结果

第一次订阅
once test
第一次订阅

上方,我们的once订阅也支持了。

发布缓存

基于以上的代码,我们考虑一种情景,如果还没有订阅者订阅事件,但我们的事件中心发布了事件,那么这时候这次发布相当于无效的。这种情况,也许是可以接受的,但是,在某些特定的场景中,我们需要将发布的事件缓存起来,等到有订阅者订阅的时候,进行调用。

思路:使用_cacheQueue来进行发布的事件的缓存,由于,我们事件发布的时候是有可能带参数的,所以,我们这个需要用一个集合/数组来存放这些参数。

type CacheArgs = Array<any>;
_cacheQueue: Map<string, Array<CacheArgs>>;

最终代码如下

interface SubscribeEvent {
  fn: Function;
  once: boolean;
}

type CacheArgs = Array<any>;

class EventEmmiter {
  subscribes: Map<string, Array<SubscribeEvent>>;
  _cacheQueue: Map<string, Array<CacheArgs>>;

  constructor() {
    this.subscribes = new Map();
    this._cacheQueue = new Map();
  }

  addEvent(type: string, callback: Function, once: boolean = false) {
    const cache = this._cacheQueue.get(type) || [];
    if(cache.length !== 0) {
      cache.forEach(args => {
        callback(...args);
      })
      this._cacheQueue.delete(type);
    }
    const sub = this.subscribes.get(type) || [];
    sub.push({ fn: callback, once });
    this.subscribes.set(type, sub);
  }

  on(type: string, callback: Function) {
    this.addEvent(type, callback);
  }

  emit(type: string, ...args: Array<any>) {
    const sub = this.subscribes.get(type) || [];
    
    if(sub.length === 0) {
      const cache = this._cacheQueue.get(type) || [];
      cache.push(args)
      this._cacheQueue.set(type, cache);
    } else {
      const context = this;
      
      sub.forEach(({ fn }) => {
        fn.call(context, ...args);
      });
  
      const newSub = sub.filter(item => !item.once);
      this.subscribes.set(type, newSub);
    }
  }

  off(type: string, callback: Function) {
    const sub = this.subscribes.get(type);
    
    if(sub) {
      const newSub = sub.filter(({ fn }) => fn !== callback);
      this.subscribes.set(type, newSub);
    }
  }

  once(type: string, callback: Function) {
    this.addEvent(type, callback, true);
  }
}

const eventEmmiter = new EventEmmiter();

eventEmmiter.emit('test_cache', 1, 2);

eventEmmiter.emit('test_cache', 1, 3);

eventEmmiter.on('test_cache', (a: number, b: number) => {
  console.log("事件发布后才订阅的, 计算的值为",a + b);
});
eventEmmiter.on('test_cache', (a: number, b: number) => {
  console.log("已没有发布事件的缓存了, 不会触发",a + b);
});

结果

事件发布后才订阅的, 计算的值为 3
事件发布后才订阅的, 计算的值为 4

上述符合预期,这里,一个简易的发布-订阅模式就完成了。

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

个人觉得这个区别,可能在于自己如何理解发布-订阅观察者模式

发布订阅模型属于广义上的观察者模式,对象中的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。

发布订阅模式是常用的一种观察者的模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式。

image-20210827222526986

  • 在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。
  • 在发布订阅模式中,发布者和订阅者多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件。

总结

上文讲述了发布-订阅的场景和简单的实现。有兴趣的小伙伴想更深入一点的话,可以去看看Node.jsEventEmitter,学习并将其应用在自己的业务场景中。

优点

  • 模块间进行解耦,不强关联与特定的其他模块,只需订阅相关事件即可。
  • 异步编程中,代码可以松耦合。

缺点

  • 松耦合弱化对象间的关系,debug时程序可能难以追踪。

最后,感谢你阅读这篇文章,写的并非很深入,可能也存在一些小错误或笔误,也请见谅,可以在下方评论区评论,不胜感激。另外,如果本文对你有一点点帮助或启发,希望可以点个赞哈,支持是创作的动力~