发布-订阅模式

128 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情

订阅-发布模式介绍

  • 发布-订阅模式 (Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。

  • 观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。

观察者模式中Subject对象一般需要实现以下API:

  • subscribe(): 接收一个观察者observer对象,使其订阅自己
  • unsubscribe(): 接收一个观察者observer对象,使其取消订阅自己
  • fire(): 触发事件,通知到所有观察者

用JavaScript手动实现观察者模式:

// 被观察者
function Subject() {
  this.observers = [];
}
Subject.prototype = {
  // 订阅
  subscribe: function (observer) {
    this.observers.push(observer);
  },
  // 取消订阅
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  },
  // 事件触发
  fire: function () {
    this.observers.forEach(observer => {
      observer.call();
    });
  }
}

使用观察者模式:

const subject = new Subject();
function observer1() {
  console.log('Observer 1 Firing!');
}
function observer2() {
  console.log('Observer 2 Firing!');
}
// 订阅多个事件
subject.subscribe(observer1);
subject.subscribe(observer2);
// 发布
subject.fire();
/** 输出:
* Observer 1 Firing! 
* Observer 2 Firing!
*/
// 取消订阅
subject.unsubscribe(observer2);

实际应用场景

  • 比如定过事件处理函数 addEventListener
document.body.addEventListener('click', function() {
    console.log('hello world!');
});
document.body.click()

通过订阅body元素的click事件,我们就可以在body的click事件触发时调用当时订阅时传入的回调函数。

  • 订阅 windowonload 事件:
window.addEventListener('load', function () {
    console.log('loaded!')
})

我们可以将一些操作挂载在 onload 事件上执行,当页面元素加载完毕,就会触发你注册在 onload 事件上的回调。虽然无法预知页面元素何时加载完毕,但是通过订阅 windowonload 事件,window 会在加载完毕时向订阅者发布消息,也就是执行回调函数。

  • Vue中的 EventBus
import Vue from 'vue'
export const EventBus = new Vue()
// 组件A
import { EventBus } from "./event-bus.js";
EventBus.$on("myevent", args => {
        console.log(args)
})
// 组件B
import { EventBus } from "./event-bus.js";
EventBus.$emit("myevent", 'some args')

我们可以自己手动实现一个eventbus:

// Bus:事件派发、监听和回调管理
class Bus {
  constructor(){
    this.callbacks = {}
  }
  $on(name, fn){
    this.callbacks[name] = this.callbacks[name] || []
    this.callbacks[name].push(fn)
  }
  $emit(name, args){
    if(this.callbacks[name]){
      this.callbacks[name].forEach(cb => cb(args))
    }
  }
}
// main.js
Vue.prototype.$bus = new Bus()
// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')
  • jQuery中的自带的 API: ontriggeroff 来轻松实现事件的订阅、发布、取消订阅
function eventHandler() {
    console.log('自定义方法')
}
// 订阅自定义事件
$('#app').on('myevent', eventHandler)
// 发布
$('#app').trigger('myevent')  // 输出:自定义方法
// 取消订阅
$('#app').off('myevent')
$('#app').trigger('myevent')  // 没有输出

订阅-发布的优缺点

最大的优点就是解耦:

  • 时间上的解耦 :注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知;
  • 对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体;

由于它的解耦特性,发布-订阅模式的使用场景一般是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变。发布-订阅模式还可以帮助实现一些其他的模式,比如中介者模式。

优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 增加了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

缺点:过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

  • 增加消耗 :创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存;
  • 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们…
  • 缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布-订阅时,情况会变得更复杂。

其他

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

观察者模式与发布-订阅者模式,在平时你可以认为他们是一个东西,但是某些场合(比如面试)下可能需要稍加注意

区别主要在发布-订阅模式中间的这个 Event Channel:

  • 观察者模式 中的观察者和被观察者之间还存在耦合,被观察者还是知道观察者的;
  • 发布-订阅模式 中的发布者和订阅者不需要知道对方的存在,他们通过消息代理来进行通信,解耦更加彻底;

image-20220416221922812.png

发布-订阅模式和责任链模式

发布-订阅模式和责任链模式也有点类似,主要区别在于:

  • 发布-订阅模式 传播的消息是根据需要随时发生变化,是发布者和订阅者之间约定的结构,在多级发布-订阅的场景下,消息可能完全不一样;
  • 责任链模式 传播的消息是不变化的,即使变化也是在原来的消息上稍加修正,不会大幅改变结构;