详解观察者模式和发布订阅模式

894 阅读5分钟

前言

在一次面试中,被问到了观察者模式和发布订阅模式有什么区别?

以我之前在网上冲浪了解过的经验,然后以灿烂的笑容回答:这两者是一样的!!

结果面试官笑着说:不,它们不一样

回到家后,通过各种渠道搜索找到答案。下面来做一个记录

观察者模式

  • 网上的解释

有一个主对象,维护其依赖者的列表,称为观察者,并自动通知他们状态的变化,通常是调用他们的一个方法

是不是很抽象,让我们用人话解释一下

假设你正在找工作,公司名是(XX公司),联系了他们的招聘经理,他向你保证,如果职位有空缺,会立刻通知你,不过除了你之外还有其他候选人,我也会顺便通知他们

在这里,XX公司是主题(Subject),他维护着观察者的列表(你和候选人),并针对事件(职位空缺)通知观察者(你们)。

下面,让我们通过代码来实现观察者模式!

页面上有一个按钮,点击的时候会在下面添加一个checkbox,这个checkbox是一个观察者, 点击全选的时候,会触发notify,实现全选的效果

<button id="add">addToCheckBox</button>
<input id="controlCheckbox" type="checkbox"/>全选
<p id="contain"></p>
// 获取dom元素
const addCheck = document.querySelector('#add') // 增加一个checkbox的按钮
const controlCheckbox = document.querySelector("#controlCheckbox") // 全选的按钮
const contain = document.querySelector('#contain') // 插入checkbox的容器

// 内部维护了一个observerList
class Subject {
  observers = new ObserverList()
  /**
   * 调用内部维护的ObserverList的add方法
   * @params {observer} observer对象
   * */
  addObserver = observer => {
    this.observers.add(observer)
  }

  /**
   * 通知函数,用于通知观察者并且执行update函数,update是一个实现接口的方法,是一个通知的触发方法。
   * */
  notify = () => {
    const len = this.observers.count() // 拿到observers的长度
    for (let i = 0; i < len; i++) {
      this.observers.get(i).update() // 调用observer的update方法
    }
  }
}

/*
  * ObserverList
  * 内部维护了一个数组,3个方法用于数组的操作,这里相关的内容还是属于subject,因为ObserverList的存在是为了将subject和内部维护的observers分离开来,清晰明了的作用。
*/
class ObserverList {
  observerList = []

  /**
   * 添加一个订阅数组
  */
  add = observer => {
    this.observerList.push(observer)
  }

  /**
   * 添加一个订阅数组
  */
  count = () => {
    return this.observerList.length
  }

  /**
   * 获取指定下标的observer
  */
  get = i => {
    return this.observerList[i]
  }
}

/*
  * The Observer
  * 提供更新接口,为想要得到通知消息的主体提供接口。
*/
class Observer {
  constructor () {
    this.update = function () {
      console.log('我被通知了')
    }
  }
}

// 合并
function extend(obj, extension){
  for (let key in extension ){
    obj[key] = extension[key];
  }
}

// 把Subject 方法合并到节点controlCheckbox中
extend(controlCheckbox, new Subject())

// 点击选中的时候更新数据
controlCheckbox.onclick = function () {
  controlCheckbox.notify() // 通知其他observer
}

addCheck.onclick = () => {
  // 创建一个Input
  const ipt = document.createElement('input')
  ipt.type = 'checkbox'

  //用观测器类扩展复选框
  extend(ipt, new Observer())

  // 用自定义更新行为重写,在这里做勾选
  ipt.update = function () {
    console.log('我被更新了')
    this.checked = !this.checked
  }

  // 将新观察者添加到我们的观察者列表中
  controlCheckbox.addObserver(ipt)

  // 添加到页面上
  contain.appendChild(ipt)
}

哼哼,代码有点多,这样就实现了观察者模式。Vue中数据响应也是用的该模式!

Pub-Sub (发布订阅者模式)

它在概念上与观察者非常相近,观察者模式的主题(Subject)就像发布者观察者(Observers)就像订阅者。实际上,两种模式的主要区别

在发布订阅者模式中,称为发布者的消息发送人不会将消息发送给直接的接收者

这意味着发布者和订阅者不知道彼此的存在,有第三方的存在(这里称为组件,也可以称为Broker),称为代理消息总线事件总线,发布者和订阅者都知道该组件,该组件会过滤所有传入的消息并相应的分发他们

按照惯例,还是举一个说人话的例子

假设你正在找工作,公司名是(XX公司),由于投递简历的人太多,招聘经理忙不过来了,招了几个HR来负责招聘,每个HR招聘的岗位不同,在让HR来给予你反馈。

在这里,发布者是招聘经理,Broker是HR,你是订阅者

下面,我们用代码来实现

const pubsub = {}

function initPubsub (pubsub) {
  // 可以广播的主题的存储
  const topics = {}

  // 标识符,每一个订阅者唯一token
  let subId = -1

  /**
   * 发布或广播感兴趣的事件
   * @param {string} topic 具有特定的主题名称
   * @param {string} args 参数
   * */
  
  pubsub.publish = function (topic, args) {
    const subscribers = topics[topic] // 拿到这个订阅者
    if (!subscribers || subscribers.length === 0) return // 如果没有这个订阅者,返回
    let len = subscribers.length // 拿到订阅者的长度
    
    // 依次调用订阅者函数
    while (len--) {
      subscribers[len].func(topic, args)
    }
    return this
  }

  /** 
   * 订阅感兴趣的事件
   * @param {string} topic 具有特定的主题名称
   * @param {function} func 回调函数
  */
  pubsub.subscribe = function (topic, func) {
    if (!topics[topic]) topics[topic] = [] // 如果没有这个订阅者,设置为空数组
    const token = (++subId).toString() // 唯一token
    topics[topic].push({ token, func }) // 存储订阅者

    // 把token返回
    return token
  }

  /** 
   * 取消订阅
   * @param {string} token
  */
  pubsub.unsubscribe = function (token) {
    for (const topic in topics) { // 循环广播存储
      const subscribers = topics[topic] 
      const index = subscribers.findIndex(({ token: subToken }) => subToken === token) // 找到这个token
      if (index === -1) continue
      subscribers.splice(index, 1) // 删除
    }
  }
  
}
// 初始化发布订阅 也可以用自执行函数
initPubsub(pubsub)

// 以下是测试代码
const sub1 = pubsub.subscribe('message', (topic, args) => {
  console.log(`类型是${topic}, 参数是:`, args)
})
const sub2 = pubsub.subscribe('hhh', (topic, args) => {
  console.log(`我是第二个啦类型是${topic}, 参数是:`, args)
})

pubsub.publish('message', 123)
pubsub.publish('message', [1,2,3])
pubsub.publish('message', { a: 1, b: 2 })

pubsub.unsubscribe(sub2)

pubsub.publish('message', 214141242)

pubsub.publish('hhh', 'hhh')

发布订阅比观察者模式要容易理解很多,哈哈,下面让我们来做一个总结!

总结

因此,这两种的主要区别可以用下图来说明。

让我们快速总结出差异:

  1. 观察者模式中,观察者知道主题,主题也维护观察者的记录。而在发布订阅模式中,发布者和订阅者不需要彼此了解。他们只是在消息队列代理的帮助下进行通信。
  2. 观察者模式,是松散耦合的,发布订阅模式相反,完全不耦合
  3. 观察者模式主要以同步方式实现,当某个事件发生时,主题调用所有观察者的方法,发布订阅模式大多是异步模式(使用消息队列)
  4. 观察者模式基本用于单个应用内部,发布订阅模式更多的是跨应用的模式。

结语

文章参考自(感谢这位大佬的文章):Observer vs Pub-Sub pattern

感谢你花了这么长时间看到这里,如果文章有任何错误的地方或者有什么建议,欢迎评论区交流!