看到一篇介绍关于观察者模式和订阅发布模式的区别的文章,看完后依然认为它们在概念和思想上是统一的,只是根据实现方式和使用场景的不同,叫法不一样,不过既然有区别,就来探究一番,加深理解。

先看图感受下两者表现出来的区别:

watcher.png

两种模型概念

观察者模式的定义是在对象之间定义一个一对多的依赖,当对象自身状态改变的时候,会自动通知给关心该状态的观察者。

解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。

这种对象与对象,有点像 商家-顾客 的关系,顾客对商家的某个商品感兴趣,就被商家记住,等有新品发布,便会直接通知顾客,相信加过微商微信会深有体会。

来张图直观感受:

watcher2.png

可以从图中看出来,这种模式是商家直接管理顾客。

订阅发布模式

该模式理解起来和观察者模式一样,也是定义一对多的依赖关系,对象状态改变后,通知给所有关心这个状态的订阅者。

订阅发布模式有订阅的动作,可以不和商家直接产生联系,只要能订阅上关心的状态即可,通常利用第三方媒介来做,而发布者也会利用三方媒介来通知订阅者。

这有点像 商家-APP-顾客 的关系,某个产品断货,顾客可以在APP上订阅上货通知,待上新,商家通过APP通知订阅的顾客。

在程序实现中,第三方媒介称之为 EventBus(事件总线),可以理解为订阅事件的集合,它提供订阅、发布、取消等功能。订阅者订阅事件,和发布者发布事件,都通过事件总线进行交互。

eventBus.png

两种模式的异同

从概念上理解,两者没什么不同,都在解决对象之间解耦,通过事件的方式在某个时间点进行触发,监听这个事件的订阅者可以进行相应的操作。

在实现上有所不同,观察者模式对订阅事件的订阅者通过发布者自身来维护,后续的一些列操作都要通过发布者完成;订阅发布模式是订阅者和发布者中间会有一个事件总线,操作都要经过事件总线完成。

观察者模式的事件名称,通常由发布者指定发布的事件,当然也可以自定义,这样看是否提供自定义的功能。

DOM 中绑定事件,click、mouseover 这些,都是内置规定好的事件名称。

  1. document.addEventListener('click',()=>{})

addEventListener 第一个参数就是绑定的时间名称;第二参数是一个函数,就是订阅者。

订阅发布模式的事件名称就比较随意,在事件总线中会维护一个事件对应的订阅者列表,当该事件触发时,会遍历列表通知所有的订阅者。

伪代码:

  1. // 订阅
  2. EventBus.on('custom', () => {})
  3. // 发布
  4. EventBus.emit('custom')

事件名称为开发者自定义,当使用频繁时维护起来较为麻烦,尤其是改名字,多个对象或组件都要替换,通常会把事件名称在一个配置中统一管理。

代码实现

观察者模式

Javascript 中函数就是对象,订阅者对象可以直接由函数来充当,就跟绑定 DOM 使用的 addEventListener 方法,第二个参数就是订阅者,是一个函数。

我们从上面描述的概念中去实现 商家-顾客,这样可以更好的理解(或者迷糊)。

定义一个顾客类,需要有个方法,这个方法用来接收商家通知的消息,就跟顾客都留有手机号码一样,发布的消息都由手机来接收,顾客收消息的方式是统一的。

  1. // 顾客
  2. class Customer {
  3. update(data){
  4. console.log('拿到了数据', data);
  5. }
  6. }

定义商家,商家提供订阅、取消订阅、发布功能

  1. // 商家
  2. class Merchant {
  3. constructor(){
  4. this.listeners = {}
  5. }
  6. addListener(name, listener){
  7. // 事件没有,定义一个队列
  8. if(this.listeners[name] === undefined) {
  9. this.listeners[name] = []
  10. }
  11. // 放在队列中
  12. this.listeners[name].push(listener)
  13. }
  14. removeListener(name, listener){
  15. // 事件没有队列,则不处理
  16. if(this.listeners[name] === undefined) return
  17. // 遍历队列,找到要移除的函数
  18. const listeners = this.listeners[name]
  19. for(let i = 0; i < listeners.length; i++){
  20. if(listeners[i] === listener){
  21. listeners.splice(i, 1)
  22. i--
  23. }
  24. }
  25. }
  26. notifyListener(name, data){
  27. // 事件没有队列,则不处理
  28. if(this.listeners[name] === undefined) return
  29. // 遍历队列,依次执行函数
  30. const listeners = this.listeners[name]
  31. for(let i = 0; i < listeners.length; i++){
  32. if(typeof listeners[i] === 'object'){
  33. listeners[i].update(data)
  34. }
  35. }
  36. }
  37. }

使用一下:

  1. // 多名顾客
  2. const c1 = new Customer()
  3. const c2 = new Customer()
  4. const c3 = new Customer()
  5. // 商家
  6. const m = new Merchant()
  7. // 顾客订阅商家商品
  8. m.addListener('shoes', c1)
  9. m.addListener('shoes', c2)
  10. m.addListener('skirt', c3)
  11. // 过了一天没来,取消订阅
  12. setTimeout(() => {
  13. m.removeListener('shoes', c2)
  14. }, 1000)
  15. // 过了几天
  16. setTimeout(() => {
  17. m.notifyListener('shoes', '来啊,购买啊')
  18. m.notifyListener('skirt', '降价了')
  19. }, 2000)

订阅发布模式

订阅和发布的功能都在事件总线中。

  1. class Observe {
  2. constructor(){
  3. this.listeners = {}
  4. }
  5. on(name, fn){
  6. // 事件没有,定义一个队列
  7. if(this.listeners[name] === undefined) {
  8. this.listeners[name] = []
  9. }
  10. // 放在队列中
  11. this.listeners[name].push(fn)
  12. }
  13. off(name, fn){
  14. // 事件没有队列,则不处理
  15. if(this.listeners[name] === undefined) return
  16. // 遍历队列,找到要移除的函数
  17. const listeners = this.listeners[name]
  18. for(let i = 0; i < this.listeners.length; i++){
  19. if(this.listeners[i] === fn){
  20. this.listeners.splice(i, 1)
  21. i--
  22. }
  23. }
  24. }
  25. emit(name, data){
  26. // 事件没有队列,则不处理
  27. if(this.listeners[name] === undefined) return
  28. // 遍历队列,依次执行函数
  29. const listenersEvent = this.listeners[name]
  30. for(let i = 0; i < listenersEvent.length; i++){
  31. if(typeof listenersEvent[i] === 'function'){
  32. listenersEvent[i](data)
  33. }
  34. }
  35. }
  36. }

使用:

  1. const observe = new Observe()
  2. // 进行订阅
  3. observe.on('say', (data) => {
  4. console.log('监听,拿到数据', data);
  5. })
  6. observe.on('say', (data) => {
  7. console.log('监听2,拿到数据', data);
  8. })
  9. // 发布
  10. setTimeout(() => {
  11. observe.emit('say', '传过去数据啦')
  12. }, 2000)

通过以上两种模式的实现上来看,观察者模式进一步抽象,能抽出公共代码就是事件总线,反过来说,如果一个对象要有观察者模式的功能,只需要继承事件总线。

node 中提供能了 events 模块可供我们灵活使用。

继承使用,都通过发布者调用:

  1. const EventEmitter = require('events')
  2. class MyEmitter extends EventEmitter {}
  3. const myEmitter = new MyEmitter()
  4. myEmitter.on('event', (data) => {
  5. console.log('触发事件', data);
  6. });
  7. myEmitter.emit('event', 1);

直接使用,当做事件总线:

  1. const EventEmitter = require('events')
  2. const emitter = new EventEmitter()
  3. emitter.on('custom', (data) => {
  4. console.log('接收数据', data);
  5. })
  6. emitter.emit('custom', 2)

应用场景

观察者模式在很多场景中都在使用,除了上述中在 DOM 上监听事件外,还有最常用的是 Vue 组件中父子之间的通信。

父级代码:

  1. <template>
  2. <div>
  3. <h2>父级</h2>
  4. <Child @custom="customHandler"></Child>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. methods: {
  10. customHandler(data){
  11. console.log('拿到数据,我要干点事', data);
  12. }
  13. }
  14. }
  15. </script>

子级代码:

  1. <template>
  2. <div>
  3. <h2>子级</h2>
  4. <button @click="clickHandler">改变了</button>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. methods: {
  10. clickHandler(){
  11. this.$emit('custome', 123)
  12. }
  13. }
  14. }
  15. </script>

子组件是一个通用的组件,内部不做业务逻辑处理,仅仅在点击时会发布一个自定义的事件 custom。子组件被使用在页面的任意地方,在不同的使用场景里,当点击按钮后子组件所在的场景会做相应的业务处理。如果关心子组件内部按钮点击这个状态的改变,只需要监听 custom 自定义事件。

订阅发布模式在用 Vue 写业务也会使用到,应用场景是在跨多层组件通信时,如果利用父子组件通信一层层订阅发布,可维护性和灵活性很差,一旦中间某个环节出问题,整个传播链路就会瘫痪。这时采用独立出来的 EventBus 解决这类问题,只要能访问到 EventBus 对象,便可通过该对象订阅和发布事件。

  1. // EventBus.js
  2. import Vue from 'vue'
  3. export default const EventBus = new Vue()

父级代码:

  1. <template>
  2. <div>
  3. <h2>父级</h2>
  4. <Child></Child>
  5. </div>
  6. </template>
  7. <script>
  8. import EventBus from './EventBus'
  9. export default {
  10. // 加载完就要监控
  11. moutend(){
  12. EventBus.on('custom', (data) => {
  13. console.log('拿到数据', data);
  14. })
  15. }
  16. }
  17. </script>
  1. <template>
  2. <div>
  3. <h2>嵌套很深的子级</h2>
  4. <button @click="clickHandler">改变了</button>
  5. </div>
  6. </template>
  7. <script>
  8. import EventBus from './EventBus'
  9. export default {
  10. methods: {
  11. clickHandler(){
  12. EventBus.emit('custom', 123)
  13. }
  14. }
  15. }
  16. </script>

通过上述代码可以看出来订阅发布模式完全解耦两个组件,互相可以不知道对方的存在,只需要在恰当的时机订阅或发布自定义事件。

订阅发布模式在 Vue2 源码中的使用

Vue2 中会通过拦截数据的获取进行依赖收集,收集的是一个个 Watcher。等待对数据进行变更时,要通知依赖的 Watcher 进行组件更新。可以通过一张图看到这个收集和通知过程。

vue.png

这些依赖存在了定义的 Dep 中,在这个类中实现了简单的订阅和发布功能,可以看做是一个 EventBus,源码如下:

  1. export default class Dep {
  2. static target: ?Watcher;
  3. id: number;
  4. subs: Array<Watcher>;
  5. constructor () {
  6. this.id = uid++
  7. this.subs = []
  8. }
  9. addSub (sub: Watcher) {
  10. this.subs.push(sub)
  11. }
  12. removeSub (sub: Watcher) {
  13. remove(this.subs, sub)
  14. }
  15. depend () {
  16. if (Dep.target) {
  17. Dep.target.addDep(this)
  18. }
  19. }
  20. notify () {
  21. // stabilize the subscriber list first
  22. const subs = this.subs.slice()
  23. for (let i = 0, l = subs.length; i < l; i++) {
  24. subs[i].update()
  25. }
  26. }
  27. }

每个 Wather 就是订阅者,这些订阅者都实现一个叫做 update 的方法,当数据更改时便会遍历所有的 Wather 调用 update 方法。

总结

通过上述的表述,相信你对观察者模式和订阅发布模式有了重新的认识,可以说二者是相同的,它们的概念和解决的问题是一样的,致力于让两个对象解耦,只是叫法不一样;也可以说二者不一样,在使用方式和场景中不一样。

参考:
https://juejin.cn/post/6844903603107266567
https://molunerfinn.com/observer-vs-pubsub-pattern/