前言

在之前两篇自测清单中,和大家分享了很多 JavaScript 基础知识,大家能够一起再回顾下~

本文是我在咱们团队外部“古代 JavaScript 突击队”分享的一篇内容,第二期学习内容为“设计模式”系列,我会将我负责分享的常识整顿成文章输入,心愿可能和大家一起温故知新!

古代 JavaScript 突击队”学习总结:

  1. 《初中级前端 JavaScript 自测清单 - 1》
  2. 《初中级前端 JavaScript 自测清单 - 2》
  3. 《TypeScript 设计模式之观察者模式》
  4. 《TypeScript语法总结+我的项目(Vue.js+TS)实战》

2.公布-订阅模式 2020.08.14

一、模式介绍

1. 生存场景

最近刚毕业的学生 Leo 筹备开始租房了,他来到房产中介,跟中介形容了本人的租房需要,开开心心回家了。第二天,中介的小哥哥小姐姐为 Leo 列出符他需要的房间,并打电话约他一起看房了,最初 Leo 选中一套称心的房间,高高兴兴过来签合同,筹备开始新生存~

还有个大佬 Paul,筹备将手中 10 套房出租进来,于是他来到房产中介,在中介那边提供了本人要出租的房间信息,沟通好手续费,开开心心回家了。第二天,Paul 接到中介的好消息,房子租出去了,于是他高高兴兴过来签合同,开始收房租了~

下面场景有个须要特地留神的中央:

  • 租户在租房过程中,不晓得房间具体房东是谁,到前面签合同才晓得;
  • 房东在出租过程中,不晓得房间具体租户是谁,到前面签合同才晓得;

这两点其实就是前面要介绍的 公布-订阅模式 的一个外围特点。

2. 概念介绍

在软件架构中,公布-订阅模式是一种音讯范式,音讯的发送者(称为发布者)不会将音讯间接发送给特定的接收者(称为订阅者)。而是将公布的音讯分为不同的类别,无需理解哪些订阅者(如果有的话)可能存在。同样的,订阅者能够表白对一个或多个类别的趣味,只接管感兴趣的音讯,无需理解哪些发布者(如果有的话)存在。

公布-订阅是音讯队列范式的兄弟,通常是更大的面向消息中间件零碎的一部分。大多数音讯零碎在API中同时反对音讯队列模型和公布/订阅模型,例如Java音讯服务(JMS)。

这种模式提供了更大的网络可扩展性和更动静的网络拓扑,同时也升高了对发布者和公布数据的构造批改的灵活性。

二、 观察者模式 vs 公布-订阅模式

看完下面概念,有没有感觉与观察者模式很像?
但其实两者还是有差别的,接下来一起看看。

1. 概念比照

咱们别离为通过两种理论生存场景来介绍这两种模式:

  • 观察者模式:如微信中 顾客-微商 关系;
  • 公布-订阅模式:如淘宝购物中 顾客-淘宝-商家 关系。

这两种场景的过程别离是这样:

1.1 观察者模式


观察者模式中,生产顾客关注(如加微信好友)本人有趣味的微商,微商就会私聊发本人在卖的产品给生产顾客。
这个过程中,生产顾客相当于观察者(Observer),微商相当于察看指标(Subject)。

1.2 公布-订阅模式

接下来看看 公布-订阅模式 :


公布-订阅模式 中,生产顾客通过淘宝搜寻本人关注的产品,商家通过淘宝公布商品,当生产顾客在淘宝搜寻的产品,曾经有商家公布,则淘宝会将对应商品举荐给生产顾客。
这个过程中,生产顾客相当于订阅者,淘宝相当于事件总线,商家相当于发布者。

2. 流程比照

3. 小结

所以能够看出,观察者模式公布-订阅模式差异在于有没有一个地方的事件总线。如果有,咱们就能够认为这是个公布-订阅模式。如果没有,那么就能够认为是观察者模式。因为其实它们都实现了一个要害的性能:公布事件-订阅事件并触发事件

三、模式特点

比照完观察者模式公布-订阅模式后,咱们大抵了解公布-订阅模式是什么了。接着总结下该模式的特点:

1. 模式组成

在公布-订阅模式中,通常蕴含以下角色:

  • 发布者:Publisher
  • 事件总线:Event Channel
  • 订阅者:Subscriber

2. UML 类图

3. 长处

  1. 松耦合(Independence)

公布-订阅模式能够将泛滥须要通信的子系统(Subsystem)解耦,每个子系统独立治理。而且即便局部子系统勾销订阅,也不会影响事件总线的整体治理。
公布-订阅模式中每个应用程序都能够专一于其外围性能,而事件总线负责将音讯路由到每个订阅者手里。

  1. 高伸缩性(Scalability)

公布-订阅模式减少了零碎的可伸缩性,进步了发布者的响应能力。起因是发布者(Publisher)能够疾速地向输出通道发送一条音讯,而后返回到其外围解决职责,而不用期待子系统解决实现。而后事件总线负责确保把消息传递到每个订阅者(Subscriber)手里。

  1. 高可靠性(Reliability)

公布-订阅模式进步了可靠性。异步的消息传递有助于应用程序在减少的负载下持续安稳运行,并且能够更无效地解决间歇性故障。

  1. 灵活性(Flexibility)

你不须要关怀不同的组件是如何组合在一起的,只有他们独特恪守一份协定即可。
公布-订阅模式容许提早解决或者按计划的解决。例如当零碎负载大的时候,订阅者能够等到非顶峰工夫才接管音讯,或者依据特定的打算解决音讯。

4. 毛病

  1. 在创立订阅者自身会耗费内存,但当订阅音讯后,没有进行公布,而订阅者会始终保留在内存中,占用内存;
  2. 创立订阅者须要耗费肯定的工夫和内存。如果适度应用的话,反而使代码不好了解及代码不好保护。

四、应用场景

如果咱们我的项目中很少应用到订阅者,或者与子系统实时交互较少,则不适宜 公布-订阅模式 。
在以下状况下能够思考应用此模式:

  1. 应用程序须要向大量消费者播送信息。例如微信订阅号就是一个消费者量宏大的播送平台。
  2. 应用程序须要与一个或多个独立开发的应用程序或服务通信,这些应用程序或服务可能应用不同的平台、编程语言和通信协议。
  3. 应用程序能够向消费者发送信息,而不须要消费者的实时响应。

五、实战示例

1. 简略示例

  1. 定义发布者接口(Publisher)、事件总线接口(EventChannel)和订阅者接口(Subscriber):
interface Publisher<T> {  subscriber: string;  data: T;}interface EventChannel<T>  {  on  : (subscriber: string, callback: () => void) => void;  off : (subscriber: string, callback: () => void) => void;  emit: (subscriber: string, data: T) => void;}interface Subscriber {  subscriber: string;  callback: () => void;}// 不便前面应用interface PublishData {  [key: string]: string;}
  1. 实现具体发布者类(ConcretePublisher):
class ConcretePublisher<T> implements Publisher<T> {  public subscriber: string = "";  public data: T;   constructor(subscriber: string, data: T) {    this.subscriber = subscriber;    this.data = data;  }}
  1. 实现具体事件总线类(ConcreteEventChannel):
class ConcreteEventChannel<T> implements EventChannel<T> {  // 初始化订阅者对象  private subjects: { [key: string]: Function[] } = {};  // 实现增加订阅事件  public on(subscriber: string, callback: () => void): void {    console.log(`收到订阅信息,订阅事件:${subscriber}`);    if (!this.subjects[subscriber]) {      this.subjects[subscriber] = [];    }    this.subjects[subscriber].push(callback);  };  // 实现勾销订阅事件  public off(subscriber: string, callback: () => void): void {    console.log(`收到勾销订阅申请,须要勾销的订阅事件:${subscriber}`);    if (callback === null) {      this.subjects[subscriber] = [];    } else {      const index: number = this.subjects[subscriber].indexOf(callback);      ~index && this.subjects[subscriber].splice(index, 1);    }  };    // 实现公布订阅事件  public emit (subscriber: string, data: T): void {    console.log(`收到发布者信息,执行订阅事件:${subscriber}`);    this.subjects[subscriber].forEach(item => item(data));  };}
  1. 实现具体订阅者类(ConcreteSubscriber):
class ConcreteSubscriber implements Subscriber {  public subscriber: string = "";  constructor(subscriber: string, callback: () => void) {    this.subscriber = subscriber;    this.callback = callback;  }  public callback(): void { };}
  1. 运行示例代码:
interface Publisher<T> {  subscriber: string;  data: T;}interface EventChannel<T>  {  on  : (subscriber: string, callback: () => void) => void;  off : (subscriber: string, callback: () => void) => void;  emit: (subscriber: string, data: T) => void;}interface Subscriber {  subscriber: string;  callback: () => void;}interface PublishData {  [key: string]: string;}class ConcreteEventChannel<T> implements EventChannel<T> {  // 初始化订阅者对象  private subjects: { [key: string]: Function[] } = {};  // 实现增加订阅事件  public on(subscriber: string, callback: () => void): void {    console.log(`收到订阅信息,订阅事件:${subscriber}`);    if (!this.subjects[subscriber]) {      this.subjects[subscriber] = [];    }    this.subjects[subscriber].push(callback);  };  // 实现勾销订阅事件  public off(subscriber: string, callback: () => void): void {    console.log(`收到勾销订阅申请,须要勾销的订阅事件:${subscriber}`);    if (callback === null) {      this.subjects[subscriber] = [];    } else {      const index: number = this.subjects[subscriber].indexOf(callback);      ~index && this.subjects[subscriber].splice(index, 1);    }  };    // 实现公布订阅事件  public emit (subscriber: string, data: T): void {    console.log(`收到发布者信息,执行订阅事件:${subscriber}`);    this.subjects[subscriber].forEach(item => item(data));  };}class ConcretePublisher<T> implements Publisher<T> {  public subscriber: string = "";  public data: T;   constructor(subscriber: string, data: T) {    this.subscriber = subscriber;    this.data = data;  }}class ConcreteSubscriber implements Subscriber {  public subscriber: string = "";  constructor(subscriber: string, callback: () => void) {    this.subscriber = subscriber;    this.callback = callback;  }  public callback(): void { };}/* 运行示例 */const pingan8787 = new ConcreteSubscriber(  "running",  () => {     console.log("订阅者 pingan8787 订阅事件胜利!执行回调~");  });const leo = new ConcreteSubscriber(  "swimming",  () => {     console.log("订阅者 leo 订阅事件胜利!执行回调~");  });const lisa = new ConcreteSubscriber(  "swimming",  () => {     console.log("订阅者 lisa 订阅事件胜利!执行回调~");  });const pual = new ConcretePublisher<PublishData>(  "swimming",  {message: "pual 公布音讯~"});const eventBus = new ConcreteEventChannel<PublishData>();eventBus.on(pingan8787.subscriber, pingan8787.callback);eventBus.on(leo.subscriber, leo.callback);eventBus.on(lisa.subscriber, lisa.callback);// 发布者 pual 公布 "swimming"相干的事件eventBus.emit(pual.subscriber, pual.data); eventBus.off (lisa.subscriber, lisa.callback);eventBus.emit(pual.subscriber, pual.data);/*输入后果:[LOG]: 收到订阅信息,订阅事件:running[LOG]: 收到订阅信息,订阅事件:swimming[LOG]: 收到订阅信息,订阅事件:swimming[LOG]: 收到发布者信息,执行订阅事件:swimming [LOG]: 订阅者 leo 订阅事件胜利!执行回调~ [LOG]: 订阅者 lisa 订阅事件胜利!执行回调~ [LOG]: 收到勾销订阅申请,须要勾销的订阅事件:swimming [LOG]: 收到发布者信息,执行订阅事件:swimming [LOG]: 订阅者 leo 订阅事件胜利!执行回调~ */

残缺代码如下:

interface Publisher {  subscriber: string;  data: any;}interface EventChannel {  on  : (subscriber: string, callback: () => void) => void;  off : (subscriber: string, callback: () => void) => void;  emit: (subscriber: string, data: any) => void;}interface Subscriber {  subscriber: string;  callback: () => void;}class ConcreteEventChannel implements EventChannel {  // 初始化订阅者对象  private subjects: { [key: string]: Function[] } = {};  // 实现增加订阅事件  public on(subscriber: string, callback: () => void): void {    console.log(`收到订阅信息,订阅事件:${subscriber}`);    if (!this.subjects[subscriber]) {      this.subjects[subscriber] = [];    }    this.subjects[subscriber].push(callback);  };  // 实现勾销订阅事件  public off(subscriber: string, callback: () => void): void {    console.log(`收到勾销订阅申请,须要勾销的订阅事件:${subscriber}`);    if (callback === null) {      this.subjects[subscriber] = [];    } else {      const index: number = this.subjects[subscriber].indexOf(callback);      ~index && this.subjects[subscriber].splice(index, 1);    }  };    // 实现公布订阅事件  public emit (subscriber: string, data = null): void {    console.log(`收到发布者信息,执行订阅事件:${subscriber}`);    this.subjects[subscriber].forEach(item => item(data));  };}class ConcretePublisher implements Publisher {  public subscriber: string = "";  public data: any;   constructor(subscriber: string, data: any) {    this.subscriber = subscriber;    this.data = data;  }}class ConcreteSubscriber implements Subscriber {  public subscriber: string = "";  constructor(subscriber: string, callback: () => void) {    this.subscriber = subscriber;    this.callback = callback;  }  public callback(): void { };}/* 运行示例 */const pingan8787 = new ConcreteSubscriber(  "running",  () => {     console.log("订阅者 pingan8787 订阅事件胜利!执行回调~");  });const leo = new ConcreteSubscriber(  "swimming",  () => {     console.log("订阅者 leo 订阅事件胜利!执行回调~");  });const lisa = new ConcreteSubscriber(  "swimming",  () => {     console.log("订阅者 lisa 订阅事件胜利!执行回调~");  });const pual = new ConcretePublisher(  "swimming",  {message: "pual 公布音讯~"});const eventBus = new ConcreteEventChannel();eventBus.on(pingan8787.subscriber, pingan8787.callback);eventBus.on(leo.subscriber, leo.callback);eventBus.on(lisa.subscriber, lisa.callback);// 发布者 pual 公布 "swimming"相干的事件eventBus.emit(pual.subscriber, pual.data); eventBus.off (lisa.subscriber, lisa.callback);eventBus.emit(pual.subscriber, pual.data);/*输入后果:[LOG]: 收到订阅信息,订阅事件:running[LOG]: 收到订阅信息,订阅事件:swimming[LOG]: 收到订阅信息,订阅事件:swimming[LOG]: 收到发布者信息,执行订阅事件:swimming [LOG]: 订阅者 leo 订阅事件胜利!执行回调~ [LOG]: 订阅者 lisa 订阅事件胜利!执行回调~ [LOG]: 收到勾销订阅申请,须要勾销的订阅事件:swimming [LOG]: 收到发布者信息,执行订阅事件:swimming [LOG]: 订阅者 leo 订阅事件胜利!执行回调~ */

2. Vue.js 应用示例

参考文章:《Vue事件总线(EventBus)应用具体介绍》 。

2.1 创立 event bus

在 Vue.js 中创立 EventBus 有两种形式:

  1. 手动实现,导出 Vue 实例化的后果。
// event-bus.jsimport Vue from 'vue'export const EventBus = new Vue();
  1. 间接在我的项目中的 main.js全局挂载 Vue 实例化的后果。
// main.jsVue.prototype.$EventBus = new Vue()

2.2 发送事件

假如你有两个Vue页面须要通信: A 和 B ,A页面按钮上绑定了点击事件,发送一则音讯,告诉 B 页面。

<!-- A.vue --><template>    <button @click="sendMsg()">-</button></template><script> import { EventBus } from "../event-bus.js";export default {  methods: {    sendMsg() {      EventBus.$emit("aMsg", '来自A页面的音讯');    }  }}; </script>

2.3 接管事件

B 页面中接管音讯,并展现内容到页面上。

<!-- IncrementCount.vue --><template>  <p>{{msg}}</p></template><script> import {   EventBus } from "../event-bus.js";export default {  data(){    return {      msg: ''    }  },  mounted() {    EventBus.$on("aMsg", (msg) => {      // A发送来的音讯      this.msg = msg;    });  }};</script>

同理能够从 B 页面往 A 页面发送音讯,应用上面办法:

// 发送音讯EventBus.$emit(channel: string, callback(payload1,…))// 监听接管音讯EventBus.$on(channel: string, callback(payload1,…))

2.4 移除事件监听者

应用 EventBus.$off('aMsg') 来移除利用内所有对此某个事件的监听。或者间接用 EventBus.$off() 来移除所有事件频道,不须要增加任何参数 。

import {   eventBus } from './event-bus.js'EventBus.$off('aMsg', {})

六、总结

观察者模式和公布-订阅模式的差异在于事件总线,如果有则是公布-订阅模式,反之为观察者模式。所以在实现公布-订阅模式,关键在于实现这个事件总线,在某个特定工夫触发某个特定事件,从而触发监听这个特定事件的组件进行相应操作的性能。公布-订阅模式在很多时候十分有用。

参考文章

1.《公布/订阅》 
2.《观察者模式VS订阅公布模式》