乐趣区

订阅发布模式和观察者模式的区别

首选我们需要先了解两者的定义和实现的方式,才能更好的区分两者的不同点。

或许以前认为订阅发布模式是观察者模式的一种别称,但是发展至今,概念已经有了不少区别。

订阅发布模式

在软件架构中,发布 - 订阅 是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

或许你用过 eventemitter、node 的 events、Backbone 的 events 等等,这些都是前端早期,比较流行的数据流通信方式,即 订阅发布模式

从字面意思来看,我们需要首先订阅,发布者发布消息后才会收到发布的消息。不过我们还需要一个中间者来协调,从事件角度来说,这个中间者就是事件中心,协调发布者和订阅者直接的消息通信。

完成订阅发布整个流程需要三个角色:

  • 发布者
  • 事件中心
  • 订阅者

以事件为例,简单流程如下:

发布者 -> 事件中心 <=> 订阅者,订阅者需要向事件中心订阅指定的事件 -> 发布者向事件中心发布指定事件内容 -> 事件中心通知订阅者 -> 订阅者收到消息(可能是多个订阅者),到此完成了一次订阅发布的流程。

简单的代码实现如下:

class Event {constructor() {
    // 所有 eventType 监听器回调函数(数组)this.listeners = {}}
  /**
   * 订阅事件
   * @param {String} eventType 事件类型
   * @param {Function} listener 订阅后发布动作触发的回调函数,参数为发布的数据
   */
  on(eventType, listener) {if (!this.listeners[eventType]) {this.listeners[eventType] = []}
    this.listeners[eventType].push(listener)
  }
  /**
   * 发布事件
   * @param {String} eventType 事件类型
   * @param {Any} data 发布的内容
   */
  emit(eventType, data) {const callbacks = this.listeners[eventType]
    if (callbacks) {callbacks.forEach((c) => {c(data)
      })
    }
  }
}

const event = new Event()
event.on('open', (data) => {console.log(data)
})
event.emit('open', { open: true})

Event 可以理解为事件中心,提供了订阅和发布功能。

订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。

观察者模式

观察者模式 定义了一种一对多的依赖关系,让多个 观察者 对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有 观察者 对象,使它们能够自动更新。

观察者模式我们可能比较熟悉的场景就是响应式数据,如 Vue 的响应式、Mbox 的响应式。

观察者模式有完成整个流程需要两个角色:

  • 目标
  • 观察者

简单流程如下:

目标 <=> 观察者,观察者观察目标(监听目标)-> 目标发生变化 -> 目标主动通知观察者。

简单的代码实现如下:

/**
 * 观察监听一个对象的变化
 * @param {Object} subject 观察的目标
 * @param {Function} callback 目标变化触发的回调
 */
function observer(subject, callback) {
  Object.defineProperty(subject, 'description', {get() {return this.data.description},
    set(val) {
      this.data.description = val
      // 目标主动通知观察者
      callback && callback(val)
    },
  })
}

可运行例子如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover"
    />
    <title></title>
  </head>
  <body>
    <div id="app">
      <div id="dom-one">
        原来的值
      </div>
      <br />
      <div id="dom-two">
        原来的值
      </div>
      <br />
      <button id="btn"> 改变 </button>
    </div>
    <script>
      /**
       * 观察监听一个对象的变化
       * @param {Object} subject 观察的目标
       * @param {Function} callback 目标变化触发的回调
       */
      function observer(subject, callback) {
        Object.defineProperty(subject, 'description', {get() {return this.data.description},
          set(val) {
            this.data.description = val
            // 目标主动通知观察者
            callback && callback(val)
          },
        })
      }

      const obj = {data: { description: ''},
      }

      observer(obj, value => {document.querySelector('#dom-one').innerHTML = value
        document.querySelector('#dom-two').innerHTML = value
      })

      btn.onclick = () => {obj.description = '改变了'}
    </script>
  </body>
</html>

两者的区别在哪?

角色角度来看,订阅发布模式需要三种角色,发布者、事件中心和订阅者。二观察者模式需要两种角色,目标和观察者,无事件中心负责通信。

从耦合度上来看,订阅发布模式是一个事件中心调度模式,订阅者和发布者是没有直接关联的,通过事件中心进行关联,两者是 解耦 的。而观察者模式中目标和观察者是直接关联的,耦合在一起(有些观念说观察者是解耦,解耦的是业务代码,不是目标和观察者本身)。

两者的优缺点?

优缺点都是从前端角度来看的。

订阅发布模式优点

  • 灵活

    由于订阅发布模式的发布者和订阅者是 解耦 的,只要引入订阅发布模式的事件中心,无论在何处都可以发布订阅。同时订阅发布者相互之间不影响。

订阅发布模式在使用不当的情况下,容易造成数据流混乱,所以才有了 React 提出的单项数据流思想,就是为了解决数据流混乱的问题。

订阅发布模式缺点

  • 容易导致代码不好维护

    灵活是有点,同时也是缺点,使用不当就会造成数据流混乱,导致代码不好维护。

  • 性能消耗更大

    订阅发布模式需要维护事件列队,订阅的事件越多,内存消耗越大。

观察者模式优点

  • 响应式

    目标变化就会通知观察者,这是观察者最大的有点,也是因为这个优点,观察者模式在前端才会这么出名。

观察者模式缺点

  • 不灵活

    相比订阅发布模式,由于目标和观察者是耦合在一起的,所以观察者模式需要同时引入目标和观察者才能达到响应式的效果。而订阅发布模式只需要引入事件中心,订阅者和发布者可以不再一处(同一个页面)。

参考文章

订阅发布模式和观察者模式真的不一样

退出移动版