前言

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

本文是我在我们团队内部“现代 JavaScript 突击队”分享的一篇内容,第二期学习内容为“设计模式”系列,我会将我负责分享的知识整理成文章输出,希望能够和大家一起温故知新!

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

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

一、模式介绍

1. 生活场景

最近刚毕业的学生 Leo 准备开始租房了,他来到房产中介,跟中介描述了自己的租房需求,开开心心回家了。第二天,中介的小哥哥小姐姐为 Leo 列出符他需求的房间,并打电话约他一起看房了,最后 Leo 选中一套满意的房间,高高兴兴过去签合同,准备开始新生活~

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

TypeScript 设计模式之发布-订阅模式 - 图1

上面场景有个需要特别注意的地方:

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

这两点其实就是后面要介绍的 发布-订阅模式 的一个核心特点。

2. 概念介绍

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

发布-订阅是消息队列范式的兄弟,通常是更大的面向消息中间件系统的一部分。大多数消息系统在API中同时支持消息队列模型和发布/订阅模型,例如Java消息服务(JMS)。

这种模式提供了更大的网络可扩展性和更动态的网络拓扑,同时也降低了对发布者和发布数据的结构修改的灵活性。

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

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

1. 概念对比

我们分别为通过两种实际生活场景来介绍这两种模式:

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

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

1.1 观察者模式

TypeScript 设计模式之发布-订阅模式 - 图2
观察者模式中,消费顾客关注(如加微信好友)自己有兴趣的微商,微商就会私聊发自己在卖的产品给消费顾客。
这个过程中,消费顾客相当于观察者(Observer),微商相当于观察目标(Subject)。

1.2 发布-订阅模式

接下来看看 发布-订阅模式

TypeScript 设计模式之发布-订阅模式 - 图3
发布-订阅模式 中,消费顾客通过淘宝搜索自己关注的产品,商家通过淘宝发布商品,当消费顾客在淘宝搜索的产品,已经有商家发布,则淘宝会将对应商品推荐给消费顾客。
这个过程中,消费顾客相当于订阅者,淘宝相当于事件总线,商家相当于发布者。

2. 流程对比

TypeScript 设计模式之发布-订阅模式 - 图4

3. 小结

所以可以看出,观察者模式发布-订阅模式差别在于有没有一个中央的事件总线。如果有,我们就可以认为这是个发布-订阅模式。如果没有,那么就可以认为是观察者模式。因为其实它们都实现了一个关键的功能:发布事件-订阅事件并触发事件

三、模式特点

对比完观察者模式发布-订阅模式后,我们大致理解发布-订阅模式是什么了。接着总结下该模式的特点:

1. 模式组成

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

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

2. UML 类图

TypeScript 设计模式之发布-订阅模式 - 图5

3. 优点

  1. 松耦合(Independence)

发布-订阅模式可以将众多需要通信的子系统(Subsystem)解耦,每个子系统独立管理。而且即使部分子系统取消订阅,也不会影响事件总线的整体管理。
发布-订阅模式中每个应用程序都可以专注于其核心功能,而事件总线负责将消息路由到每个订阅者手里。

  1. 高伸缩性(Scalability)

发布-订阅模式增加了系统的可伸缩性,提高了发布者的响应能力。原因是发布者(Publisher)可以快速地向输入通道发送一条消息,然后返回到其核心处理职责,而不必等待子系统处理完成。然后事件总线负责确保把消息传递到每个订阅者(Subscriber)手里。

  1. 高可靠性(Reliability)

发布-订阅模式提高了可靠性。异步的消息传递有助于应用程序在增加的负载下继续平稳运行,并且可以更有效地处理间歇性故障。

  1. 灵活性(Flexibility)

你不需要关心不同的组件是如何组合在一起的,只要他们共同遵守一份协议即可。
发布-订阅模式允许延迟处理或者按计划的处理。例如当系统负载大的时候,订阅者可以等到非高峰时间才接收消息,或者根据特定的计划处理消息。

4. 缺点

  1. 在创建订阅者本身会消耗内存,但当订阅消息后,没有进行发布,而订阅者会一直保存在内存中,占用内存;
  2. 创建订阅者需要消耗一定的时间和内存。如果过度使用的话,反而使代码不好理解及代码不好维护。

四、使用场景

如果我们项目中很少使用到订阅者,或者与子系统实时交互较少,则不适合 发布-订阅模式
在以下情况下可以考虑使用此模式:

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

五、实战示例

1. 简单示例

  1. 定义发布者接口(Publisher)、事件总线接口(EventChannel)和订阅者接口(Subscriber):
  1. interface Publisher<T> {
  2. subscriber: string;
  3. data: T;
  4. }
  5. interface EventChannel<T> {
  6. on : (subscriber: string, callback: () => void) => void;
  7. off : (subscriber: string, callback: () => void) => void;
  8. emit: (subscriber: string, data: T) => void;
  9. }
  10. interface Subscriber {
  11. subscriber: string;
  12. callback: () => void;
  13. }
  14. // 方便后面使用
  15. interface PublishData {
  16. [key: string]: string;
  17. }
  1. 实现具体发布者类(ConcretePublisher):
  1. class ConcretePublisher<T> implements Publisher<T> {
  2. public subscriber: string = "";
  3. public data: T;
  4. constructor(subscriber: string, data: T) {
  5. this.subscriber = subscriber;
  6. this.data = data;
  7. }
  8. }
  1. 实现具体事件总线类(ConcreteEventChannel):
  1. class ConcreteEventChannel<T> implements EventChannel<T> {
  2. // 初始化订阅者对象
  3. private subjects: { [key: string]: Function[] } = {};
  4. // 实现添加订阅事件
  5. public on(subscriber: string, callback: () => void): void {
  6. console.log(`收到订阅信息,订阅事件:${subscriber}`);
  7. if (!this.subjects[subscriber]) {
  8. this.subjects[subscriber] = [];
  9. }
  10. this.subjects[subscriber].push(callback);
  11. };
  12. // 实现取消订阅事件
  13. public off(subscriber: string, callback: () => void): void {
  14. console.log(`收到取消订阅请求,需要取消的订阅事件:${subscriber}`);
  15. if (callback === null) {
  16. this.subjects[subscriber] = [];
  17. } else {
  18. const index: number = this.subjects[subscriber].indexOf(callback);
  19. ~index && this.subjects[subscriber].splice(index, 1);
  20. }
  21. };
  22. // 实现发布订阅事件
  23. public emit (subscriber: string, data: T): void {
  24. console.log(`收到发布者信息,执行订阅事件:${subscriber}`);
  25. this.subjects[subscriber].forEach(item => item(data));
  26. };
  27. }
  1. 实现具体订阅者类(ConcreteSubscriber):
  1. class ConcreteSubscriber implements Subscriber {
  2. public subscriber: string = "";
  3. constructor(subscriber: string, callback: () => void) {
  4. this.subscriber = subscriber;
  5. this.callback = callback;
  6. }
  7. public callback(): void { };
  8. }
  1. 运行示例代码:
  1. interface Publisher<T> {
  2. subscriber: string;
  3. data: T;
  4. }
  5. interface EventChannel<T> {
  6. on : (subscriber: string, callback: () => void) => void;
  7. off : (subscriber: string, callback: () => void) => void;
  8. emit: (subscriber: string, data: T) => void;
  9. }
  10. interface Subscriber {
  11. subscriber: string;
  12. callback: () => void;
  13. }
  14. interface PublishData {
  15. [key: string]: string;
  16. }
  17. class ConcreteEventChannel<T> implements EventChannel<T> {
  18. // 初始化订阅者对象
  19. private subjects: { [key: string]: Function[] } = {};
  20. // 实现添加订阅事件
  21. public on(subscriber: string, callback: () => void): void {
  22. console.log(`收到订阅信息,订阅事件:${subscriber}`);
  23. if (!this.subjects[subscriber]) {
  24. this.subjects[subscriber] = [];
  25. }
  26. this.subjects[subscriber].push(callback);
  27. };
  28. // 实现取消订阅事件
  29. public off(subscriber: string, callback: () => void): void {
  30. console.log(`收到取消订阅请求,需要取消的订阅事件:${subscriber}`);
  31. if (callback === null) {
  32. this.subjects[subscriber] = [];
  33. } else {
  34. const index: number = this.subjects[subscriber].indexOf(callback);
  35. ~index && this.subjects[subscriber].splice(index, 1);
  36. }
  37. };
  38. // 实现发布订阅事件
  39. public emit (subscriber: string, data: T): void {
  40. console.log(`收到发布者信息,执行订阅事件:${subscriber}`);
  41. this.subjects[subscriber].forEach(item => item(data));
  42. };
  43. }
  44. class ConcretePublisher<T> implements Publisher<T> {
  45. public subscriber: string = "";
  46. public data: T;
  47. constructor(subscriber: string, data: T) {
  48. this.subscriber = subscriber;
  49. this.data = data;
  50. }
  51. }
  52. class ConcreteSubscriber implements Subscriber {
  53. public subscriber: string = "";
  54. constructor(subscriber: string, callback: () => void) {
  55. this.subscriber = subscriber;
  56. this.callback = callback;
  57. }
  58. public callback(): void { };
  59. }
  60. /* 运行示例 */
  61. const pingan8787 = new ConcreteSubscriber(
  62. "running",
  63. () => {
  64. console.log("订阅者 pingan8787 订阅事件成功!执行回调~");
  65. }
  66. );
  67. const leo = new ConcreteSubscriber(
  68. "swimming",
  69. () => {
  70. console.log("订阅者 leo 订阅事件成功!执行回调~");
  71. }
  72. );
  73. const lisa = new ConcreteSubscriber(
  74. "swimming",
  75. () => {
  76. console.log("订阅者 lisa 订阅事件成功!执行回调~");
  77. }
  78. );
  79. const pual = new ConcretePublisher<PublishData>(
  80. "swimming",
  81. {message: "pual 发布消息~"}
  82. );
  83. const eventBus = new ConcreteEventChannel<PublishData>();
  84. eventBus.on(pingan8787.subscriber, pingan8787.callback);
  85. eventBus.on(leo.subscriber, leo.callback);
  86. eventBus.on(lisa.subscriber, lisa.callback);
  87. // 发布者 pual 发布 "swimming"相关的事件
  88. eventBus.emit(pual.subscriber, pual.data);
  89. eventBus.off (lisa.subscriber, lisa.callback);
  90. eventBus.emit(pual.subscriber, pual.data);
  91. /*
  92. 输出结果:
  93. [LOG]: 收到订阅信息,订阅事件:running
  94. [LOG]: 收到订阅信息,订阅事件:swimming
  95. [LOG]: 收到订阅信息,订阅事件:swimming
  96. [LOG]: 收到发布者信息,执行订阅事件:swimming
  97. [LOG]: 订阅者 leo 订阅事件成功!执行回调~
  98. [LOG]: 订阅者 lisa 订阅事件成功!执行回调~
  99. [LOG]: 收到取消订阅请求,需要取消的订阅事件:swimming
  100. [LOG]: 收到发布者信息,执行订阅事件:swimming
  101. [LOG]: 订阅者 leo 订阅事件成功!执行回调~
  102. */

完整代码如下:

  1. interface Publisher {
  2. subscriber: string;
  3. data: any;
  4. }
  5. interface EventChannel {
  6. on : (subscriber: string, callback: () => void) => void;
  7. off : (subscriber: string, callback: () => void) => void;
  8. emit: (subscriber: string, data: any) => void;
  9. }
  10. interface Subscriber {
  11. subscriber: string;
  12. callback: () => void;
  13. }
  14. class ConcreteEventChannel implements EventChannel {
  15. // 初始化订阅者对象
  16. private subjects: { [key: string]: Function[] } = {};
  17. // 实现添加订阅事件
  18. public on(subscriber: string, callback: () => void): void {
  19. console.log(`收到订阅信息,订阅事件:${subscriber}`);
  20. if (!this.subjects[subscriber]) {
  21. this.subjects[subscriber] = [];
  22. }
  23. this.subjects[subscriber].push(callback);
  24. };
  25. // 实现取消订阅事件
  26. public off(subscriber: string, callback: () => void): void {
  27. console.log(`收到取消订阅请求,需要取消的订阅事件:${subscriber}`);
  28. if (callback === null) {
  29. this.subjects[subscriber] = [];
  30. } else {
  31. const index: number = this.subjects[subscriber].indexOf(callback);
  32. ~index && this.subjects[subscriber].splice(index, 1);
  33. }
  34. };
  35. // 实现发布订阅事件
  36. public emit (subscriber: string, data = null): void {
  37. console.log(`收到发布者信息,执行订阅事件:${subscriber}`);
  38. this.subjects[subscriber].forEach(item => item(data));
  39. };
  40. }
  41. class ConcretePublisher implements Publisher {
  42. public subscriber: string = "";
  43. public data: any;
  44. constructor(subscriber: string, data: any) {
  45. this.subscriber = subscriber;
  46. this.data = data;
  47. }
  48. }
  49. class ConcreteSubscriber implements Subscriber {
  50. public subscriber: string = "";
  51. constructor(subscriber: string, callback: () => void) {
  52. this.subscriber = subscriber;
  53. this.callback = callback;
  54. }
  55. public callback(): void { };
  56. }
  57. /* 运行示例 */
  58. const pingan8787 = new ConcreteSubscriber(
  59. "running",
  60. () => {
  61. console.log("订阅者 pingan8787 订阅事件成功!执行回调~");
  62. }
  63. );
  64. const leo = new ConcreteSubscriber(
  65. "swimming",
  66. () => {
  67. console.log("订阅者 leo 订阅事件成功!执行回调~");
  68. }
  69. );
  70. const lisa = new ConcreteSubscriber(
  71. "swimming",
  72. () => {
  73. console.log("订阅者 lisa 订阅事件成功!执行回调~");
  74. }
  75. );
  76. const pual = new ConcretePublisher(
  77. "swimming",
  78. {message: "pual 发布消息~"}
  79. );
  80. const eventBus = new ConcreteEventChannel();
  81. eventBus.on(pingan8787.subscriber, pingan8787.callback);
  82. eventBus.on(leo.subscriber, leo.callback);
  83. eventBus.on(lisa.subscriber, lisa.callback);
  84. // 发布者 pual 发布 "swimming"相关的事件
  85. eventBus.emit(pual.subscriber, pual.data);
  86. eventBus.off (lisa.subscriber, lisa.callback);
  87. eventBus.emit(pual.subscriber, pual.data);
  88. /*
  89. 输出结果:
  90. [LOG]: 收到订阅信息,订阅事件:running
  91. [LOG]: 收到订阅信息,订阅事件:swimming
  92. [LOG]: 收到订阅信息,订阅事件:swimming
  93. [LOG]: 收到发布者信息,执行订阅事件:swimming
  94. [LOG]: 订阅者 leo 订阅事件成功!执行回调~
  95. [LOG]: 订阅者 lisa 订阅事件成功!执行回调~
  96. [LOG]: 收到取消订阅请求,需要取消的订阅事件:swimming
  97. [LOG]: 收到发布者信息,执行订阅事件:swimming
  98. [LOG]: 订阅者 leo 订阅事件成功!执行回调~
  99. */

2. Vue.js 使用示例

参考文章:《Vue事件总线(EventBus)使用详细介绍》

2.1 创建 event bus

在 Vue.js 中创建 EventBus 有两种方式:

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

2.2 发送事件

假设你有两个Vue页面需要通信: A 和 B ,A页面按钮上绑定了点击事件,发送一则消息,通知 B 页面。

  1. <!-- A.vue -->
  2. <template>
  3. <button @click="sendMsg()">-</button>
  4. </template>
  5. <script>
  6. import { EventBus } from "../event-bus.js";
  7. export default {
  8. methods: {
  9. sendMsg() {
  10. EventBus.$emit("aMsg", '来自A页面的消息');
  11. }
  12. }
  13. };
  14. </script>

2.3 接收事件

B 页面中接收消息,并展示内容到页面上。

  1. <!-- IncrementCount.vue -->
  2. <template>
  3. <p>{{msg}}</p>
  4. </template>
  5. <script>
  6. import {
  7. EventBus
  8. } from "../event-bus.js";
  9. export default {
  10. data(){
  11. return {
  12. msg: ''
  13. }
  14. },
  15. mounted() {
  16. EventBus.$on("aMsg", (msg) => {
  17. // A发送来的消息
  18. this.msg = msg;
  19. });
  20. }
  21. };
  22. </script>

同理可以从 B 页面往 A 页面发送消息,使用下面方法:

  1. // 发送消息
  2. EventBus.$emit(channel: string, callback(payload1,…))
  3. // 监听接收消息
  4. EventBus.$on(channel: string, callback(payload1,…))

2.4 移除事件监听者

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

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

六、总结

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

参考文章

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