Skip to content

发布订阅模式

很多文章都把 发布订阅模式观察者模式区别开来,从根本来说是同一种设计模式。抛开代码,假设我们订阅某个频道的咨询,一旦有新资讯发布,就能立即收到通知。我们只需要订阅某个事件即可。

我们经常使用的 DOM 事件,使用的就是发布订阅模式,我们逐步解析:

摘录来自: 曾探. “JavaScript设计模式与开发实践 (图灵原创)。

js
document.body.addEventListener( 'click', function(){
    alert(2);
}, false );

document.body.click();    // 模拟用户点击”

如上代码,我们订阅 body 上面的点击事件,并告知事件中心要执行一个回调函数。当触发点击事件的时候,事件中心会触发我们注册的回调函数。

回想 node 的发布订阅模式,有一个事件中心 EventEmitter:

js
var EventEmitter = require('events').EventEmitter; 
var event = new EventEmitter(); 
event.on('hello', function() { 
    console.log('hello 大家好'); 
}); 
setTimeout(function() { 
    event.emit('hello'); 
}, 1000);

上述代码 通过 event.on 注册了 hello 事件,一秒后又触发了这个事件,并执行了事件对应的回调。参照上述例子,我们 使用 on 方法注册监听,emit 方法触发监听,Let's go:

js
class MyEvent {
  // 存储所有注册的事件
  eventObj = {}

  /**
   * 注册回调
   * @param {*} type 事件类型
   * @param {*} cb 回调函数
   */
  on(type, cb) {
    // 如果注册类型不存在,初始化为一个数组
    if (!this.eventObj[type]) {
      this.eventObj[type] = []
    }
    // 推入数组
    this.eventObj[type].push(cb)
  }
  /**
   * 触发订阅事件
   * @param {*} type 事件类型
   */
  emit(type, ...args) {
    // 触发所有注册的事件
    this.eventObj[type] && this.eventObj[type].forEach(cb => cb.apply(this, args))
  }
}
const eventCenter = new MyEvent()

// 订阅事件类型
eventCenter.on('hello', () => {
  console.log('hello 事件被触发');
})
eventCenter.on('hello', () => {
  console.log('hello 事件第二次被触发');
})

setTimeout(() => {
  // 触发事件
  eventCenter.emit('hello')
}, 1000)

上述代码是可以跑通的,你可以试着注册不同的事件验证效果。 如果你熟练用过发布订阅模式的库,你会发现除了订阅还能取消订阅,一般是 off 方法,下面我们来完善这个方法。

js
  /**
   * 取消订阅
   * @param {*} type 事件类型
   * @param {*} cb 回调函数 可选
   */
  off(type, cb) {
    // 如果没有传入指定的函数,代表取消所有监听
    const fns = this.eventObj[type]
    if (!fns) return false
    if (!cb) {
      this.eventObj[type] = []
    } else {
      return fns.some((fn, i) => {
        if (fn === cb) {
          fns.splice(i, 1)
          return true
        }
      })
    }
  }
}

我们验证是否成功:

js

// 订阅事件类型
eventCenter.on('hello', () => {
  console.log('hello 事件被触发');
})
const secondFn = () => {
  console.log('hello 事件第二次被触发');
}
eventCenter.on('hello', secondFn)

setTimeout(() => {
  eventCenter.off('hello', secondFn)
  // 触发事件
  eventCenter.emit('hello')
}, 1000)

能看到我们赋予了匿名的箭头函数一个变量引用 secondFn,这对应了 off 函数里面第二个参数,只要订阅的函数和取消订阅的函数属于同一个引用,就能取消订阅。如果订阅的是一个匿名函数,是无法单独取消的,因为取消订阅的时候无法拿到匿名函数的引用,只能 eventCenter.on('hello') 取消 hello 订阅事件的所有订阅。