乐趣区

关于javascript:TypeScript-设计模式之发布订阅模式

前言

在之前两篇自测清单中,和大家分享了很多 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.js
import Vue from 'vue'
export const EventBus = new Vue();
  1. 间接在我的项目中的 main.js全局挂载 Vue 实例化的后果。
// main.js
Vue.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 订阅公布模式》

退出移动版