一看就懂的 JavaScript 设计模式之发布订阅者模式

139 阅读3分钟

发布订阅者模式

定义

基于一个主题/事件通道,希望接收通知的对象通过自定义事件订阅主题,被激活事件的对象通过发布主题事件的方式被通知。

发布订阅者模式是一种对象间一对多的依赖关系(利用消息队列)。当一个对象的状态发生改变时,所有依赖于它的对象都得到状态改变的通知。

场景

小明、小红、小林、小赵打算报补习班,小明、小红报的英语补习班,小林、小赵报的数学补习班,但现在补习班还没开课。

补习班负责人说:你们可以先把手机号码留下,等要开课了我再给你们打电话。

于是英语补习班开课了,负责人就给小明和小红打电话了

数学补习班开课了,负责人就给小林和小赵打电话了

我们用发布订阅者模式去实现上述的场景,那么补习班负责人就是发布者,他发布补习班开课的通知给对应的人,小明、小红、小林、小赵我们是属于订阅者,我们订阅我们要去的补习班的开课通知,当我们订阅的课要开始了,发布者就会通知我们。

代码实现

思路

  • 创建一个类。
  • 在该类上创建一个缓存列表(调度中心)。
  • 要有一个on方法来把函数fn都加到缓存列表中,也就是订阅者注册时间到调度中心。
  • 要有一个 emit 方法取到 event 事件类型,根据 event 值去执行对应缓存列表中的函数,也就是发布者发布事件到调度中心,调度中心处理代码。
  • 要有一个 off 方法,根据 event 事件类型取消订阅。

代码

class Observer {
  constructor() {
    // 所有订阅的消息都存入到这个调度中心
    this.subjects = {}
  }
  // 消息订阅
  $on(type, fn) {
    // 判断调度中心是否存在这个属性,不存在就初始化一个空数组
    if (!this.subjects[type]) this.subjects[type] = []
    // 向数组中添加传入的订阅回调
    this.subjects[type].push(fn)
  }
  // 消息发布
  $emit(type) {
    // 判断调度中心是否存在此订阅,没有就结束
    if (!this.subjects[type]) return
    // 循环执行此订阅里的所有消息
    this.subjects[type].forEach(item => item())
  }
  // 取消订阅
  $off(type, fn) {
    // 判断调度中心是否存在此订阅,没有就结束
    if (!this.subjects[type]) return
    // 判断是否传入具体fn, 若未穿入直接删除整个消息队列
    if (!fn) this.subjects[type] = undefined
    // 若有具体fn,删除订阅里的此方法
    else {
      this.subjects[type] = this.subjects[type].filter(item => item !== fn)
    }
  }
}

上面是一个我们实现的发布订阅者模式实例,我们现在把我们的场景带入看下:

let observer = new Observer()
​
let fn1= function () {
  console.log('小明的手机号码:11345165432')
}
let fn2= function () {
  console.log('小红的手机号码:12345165432')
}
let fn3= function () {
  console.log('小赵的手机号码:12345665332')
}
let fn4= function () {
  console.log('小林的手机号码:12345665412')
}
​
// 现在去订阅每个人的课程
observer.$on('English', fn1)
observer.$on('English', fn2)
observer.$on('maths', fn3)
observer.$on('maths', fn4)
// 这里就实现了 课程的订阅// 比如当英语要开课的时候,负责人去通知上英语课的同学
observer.$emit('English')
// log: 小明的手机号码:11345165432
// log: 小红的手机号码:12345165432
// 这样就通知了要上英语课的同学// 现在比如小赵有别的事情导致他不能去上数学补习班,那么我们取消小赵订阅的事件
observer.$off('maths',fn3 )
// 这样当通知数学要开课的时候就只会通知小林了,因为小赵有事取消了数学课的订阅
observer.$off('maths',fn3 )
// log: 小林的手机号码:12345665412// 现在补习班突发情况导致英语没法开课了,那么负责人就取消要英语课上面所有的订阅了
// 我们上面所封装了这种情况,只需传入属性名字即可,这样就取消了这个属性上的所有消息
observer.$off('English')
​

总结

发布订阅者模式:

  • 发布订阅模式中,发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,互不关心对方是谁。
  • 松散耦合,灵活度高,常用作事件总线。

如果项目中存在多对一的依赖,并且每个模块都相对独立,那么可以考虑使用发布订阅模式来重构代码。而发布订阅模式是前端开发中最常用的设计模式,很多框架都有用到这一设计模式。

案例: Vue事件总线(EventBus)

Vue双向数据绑定也结合了这一思想

GitHub

https://github.com/zdongfeng/Pro-JavaScript-Design-Patterns