JavaScript常见设计模式——发布、订阅模式 | 青训营笔记

125 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第5天

一、 什么是发布-订阅模式

1. 定义

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

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

2. 例子

比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

二、 如何实现发布-订阅模式?

1. 实现思路

  • 创建一个对象
  • 在该对象上创建一个缓存列表(调度中心)
  • on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
  • emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
  • off 方法可以根据 event 值取消订阅(取消订阅)
  • once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
let eventEmitter = {
  // 缓存列表
  list: {},
  // 订阅
  on(event, fn) {
    let _this = this
    // 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
    // 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
    ;(_this.list[event] || (_this.list[event] = [])).push(fn)
    return _this
  },
  // 监听一次
  once(event, fn) {
    // 先绑定,调用后删除
    let _this = this
    function on() {
      _this.off(event, on)
      fn.apply(_this, arguments)
    }
    on.fn = fn
    _this.on(event, on)
    return _this
  },
  // 取消订阅
  off(event, fn) {
    let _this = this
    let fns = _this.list[event]
    // 如果缓存列表中没有相应的 fn,返回false
    if (!fns) return false
    if (!fn) {
      // 如果没有传 fn 的话,就会将 event 值对应缓存列表中的 fn 都清空
      fns && (fns.length = 0)
    } else {
      // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可
      let cb
      for (let i = 0, cbLen = fns.length; i < cbLen; i++) {
        cb = fns[i]
        if (cb === fn || cb.fn === fn) {
          fns.splice(i, 1)
          break
        }
      }
    }
    return _this
  },
  // 发布
  emit() {
    let _this = this
    // 第一个参数是对应的 event 值,直接用数组的 shift 方法取出
    let event = [].shift.call(arguments),
      fns = [..._this.list[event]]
    // 如果缓存列表里没有 fn 就返回 false
    if (!fns || fns.length === 0) {
      return false
    }
    // 遍历 event 值对应的缓存列表,依次执行 fn
    fns.forEach((fn) => {
      fn.apply(_this, arguments)
    })
    return _this
  },
}

function user1(content) {
  console.log('用户1订阅了:', content)
}

function user2(content) {
  console.log('用户2订阅了:', content)
}

function user3(content) {
  console.log('用户3订阅了:', content)
}

function user4(content) {
  console.log('用户4订阅了:', content)
}

// 订阅
eventEmitter.on('article1', user1)
eventEmitter.on('article1', user2)
eventEmitter.on('article1', user3)

// 取消user2方法的订阅
eventEmitter.off('article1', user2)
// 只监听一次
eventEmitter.once('article2', user4)

// 发布
eventEmitter.emit('article1', 'Javascript 发布-订阅模式')
eventEmitter.emit('article1', 'Javascript 发布-订阅模式')
eventEmitter.emit('article2', 'Javascript 观察者模式')
eventEmitter.emit('article2', 'Javascript 观察者模式')

执行结果:由结果可以分析,因为user1和user3订阅了article1,所以当后面调用2次emit发布article1的时候,user1和user3都会被执行,虽然user2也订阅了article1,但后面取消了订阅,所以user2不会执行,而user4通过once订阅了article2,虽然后面发布了两次article2,但user4只执行了一次。

image.png