发布订阅模式

87 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

1、什么是发布订阅模式

发布订阅模式主要由发布者、订阅者、消息通道三部分组成,多个订阅者可以同时监听一个发布者,当发布者状态发生变化时,就会通过消息通道将新的状态广播给各订阅者

一个实际生活中的小🌰子:

小明和同学都在掘金上关注了某一场公开课,当公开课即将开始的某段时间内,运营(假设)会编辑消息推送到各个同学的账号,提醒同学去观看。

在这个过程中,小明和同学们就是订阅者,运营就是发布者,而掘金平台就扮演了消息通道的角色。

2、前端实现发布订阅模式

  • 定义消息通道(消息队列)

  • 定义订阅者监听发布者方法

  • 定义发布者通知订阅者方法

  • 定义订阅者取消监听发布者方法

class EventEmitter {
  constructor() {
    this.events = Object.create(null);
  }

  /**
  * 事件监听 
  * @param {String} event 事件名 
  * @param {Function} fn 回调函数 
  */
  on(event, fn) {
    (this.events[event] || (this.events[event] = [])).push(fn);
    return this;
  }

  /**
  * 事件发布
  * @param {String} event 事件名
  */
  emit(event, ...args) {
    let cbs = this.events[event];
    if(cbs) {
      for (let i = 0, l = cbs.length; i < l; i++) {
        cbs[i].apply(this, args);
      }
    }
  }

  /** 取消事件监听
  * @param {String} event 事件名 
  * @param {Function} fn 回调函数名 
  */
  off(event, fn) {
    // this.off()   清空所有监听函数
    if(!arguments.length) {
      this.events = Object.create(null);
    }

    // this.off(event)  清空该事件的监听函数
    if(!fn) {
      this.events[event] = null;
    }

    let cbs = this.events[event]
    if(!cbs) {
      return this;
    }

    let cb;
    let i = cbs.length;
    while(i --) {
      cb = cbs[i]
      // XXX: 待补充,当使用once监听事件时,此处判断需要改为 (cb === fn || cb.fn === fn)
      if(cb === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
}

至此,一个基本的发布订阅模式就实现了,具体用法如下:


// 定义回调函数,供后续使用
const f1 = function(a) {
  console.log('关注的公开课f1马上就要开始啦!', a);
}
const f2 = function(a) {
  console.log('关注的公开课f2马上就要开始啦!', a);
}
const f3 = function(a, b) {
  console.log('关注的公开课f3马上就要开始啦!');
  console.log(a + b);
}
    
    

// 创建一个Event实例
const e = new EventEmitter();
// 注册监听事件(订阅者)
e.on('follow', f1);
e.on('follow', f2);
e.on('follow', f3);

// 发布事件(发布者):
// 所有监听了 follow 事件的订阅者都会执行对应的回调函数,即上述的 f1 f2 f3。3,7会作为回调函数的形参传入
e.emit('follow', 3, 7);  // ==》 关注的公开课f1马上就要开始啦!3,关注的公开课f2马上就要开始啦!3,关注的公开课f3马上就要开始啦!10

// 注册监听事件 jym,回调函数:f1,f2
e.on('jym', f1);
e.on('jym', f2);
// 发布事件 jym,f1、f2执行
e.emit('jym');
// 取消监听事件 jym的回调函数f1
e.off('jym', f1);
// 只执行 f2
e.emit('jym');
  • 扩展:定义once方法,监听事件发布后失效,即只执行一次回调函数
  /** 取消事件监听
  * @param {String} event 事件名 
  * @param {Function} fn 回调函数名 
  */
  off(event, fn) {
    // this.off()   清空所有监听函数
    if(!arguments.length) {
      this.events = Object.create(null);
    }

    // this.off(event)  清空该事件的监听函数
    if(!fn) {
      this.events[event] = null;
    }

    let cbs = this.events[event]
    if(!cbs) {
      return this;
    }

    let cb;
    let i = cbs.length;
    while(i --) {
      cb = cbs[i]
      // 如果注册的监听事件是once,则原回调函数存在cb.fn中
      if(cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
  
   /**
    * 监听事件,只执行一次回调函数
    * @param {String} event 事件名
    * @param {Function} fn 回调函数 
    */
  once(event, fn) {
    // 最终注册监听事件的回调函数是该方法
    function on(...args) {
      // 利用off方法取消监听事件
      this.off(event, on);
      fn.apply(this, args);
    }
    // 缓存原回调函数
    on.fn = fn;
    // 利用this.on方法监听事件
    this.on(event, on);
    return this;
  }

测试代码:

e.once('jym', f1);
e.emit('jym');
e.emit('jym');
// 结果:只执行了一次f1方法

总结

平时工作中很难用到这些经典的设计模式,最近看Vue2源码中用到了该模式实现eventbus,就找了一些发布订阅模式相关的文章学习,在此记录一下。