
前言
在之前兩篇自測清單中,和大家分享了很多 JavaScript 基礎知識,大家可以一起再回顧下~
本文是我在我們團隊内部“「現代 JavaScript 突擊隊」”分享的一篇内容,第二期學習内容為“「設計模式」”系列,我會将我負責分享的知識整理成文章輸出,希望能夠和大家一起溫故知新!
一、模式介紹
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 類圖
釋出-訂閱模式(UML)
3. 優點
- 松耦合(Independence)
「釋出-訂閱模式」可以将衆多需要通信的子系統(Subsystem)解耦,每個子系統獨立管理。而且即使部分子系統取消訂閱,也不會影響「事件總線」的整體管理。「釋出-訂閱模式」中每個應用程式都可以專注于其核心功能,而「事件總線」負責将消息路由到每個「訂閱者」手裡。
- 高伸縮性(Scalability)
「釋出-訂閱模式」增加了系統的可伸縮性,提高了釋出者的響應能力。原因是「釋出者」(Publisher)可以快速地向輸入通道發送一條消息,然後傳回到其核心處理職責,而不必等待子系統處理完成。然後「事件總線」負責確定把消息傳遞到每個「訂閱者」(Subscriber)手裡。
- 高可靠性(Reliability)
「釋出-訂閱模式」提高了可靠性。異步的消息傳遞有助于應用程式在增加的負載下繼續平穩運作,并且可以更有效地處理間歇性故障。
- 靈活性(Flexibility)
你不需要關心不同的元件是如何組合在一起的,隻要他們共同遵守一份協定即可。「釋出-訂閱模式」允許延遲處理或者按計劃的處理。例如當系統負載大的時候,訂閱者可以等到非高峰時間才接收消息,或者根據特定的計劃處理消息。
4. 缺點**
- 在建立訂閱者本身會消耗記憶體,但當訂閱消息後,沒有進行釋出,而訂閱者會一直儲存在記憶體中,占用記憶體;
- 建立訂閱者需要消耗一定的時間和記憶體。如果過度使用的話,反而使代碼不好了解及代碼不好維護。
四、使用場景
如果我們項目中很少使用到訂閱者,或者與子系統實時互動較少,則不适合 「釋出-訂閱模式」 。在以下情況下可以考慮使用此模式:
- 應用程式需要「向大量消費者廣播資訊」。例如微信訂閱号就是一個消費者量龐大的廣播平台。
- 應用程式需要與一個或多個獨立開發的應用程式或服務「通信」,這些應用程式或服務可能使用不同的平台、程式設計語言和通信協定。
- 應用程式可以向消費者發送資訊,而不需要消費者的實時響應。
五、實戰示例
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;
}
- 實作「具體釋出者類」(ConcretePublisher):
class ConcretePublisher<T> implements Publisher<T> {
public subscriber: string = "";
public data: T;
constructor(subscriber: string, data: T) {
this.subscriber = subscriber;
this.data = data;
}
}
- 實作「具體事件總線類」(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));
};
}
- 實作「具體訂閱者類」(ConcreteSubscriber):
class ConcreteSubscriber implements Subscriber {
public subscriber: string = "";
constructor(subscriber: string, callback: () => void) {
this.subscriber = subscriber;
this.callback = callback;
}
public callback(): void { };
}
- 運作示例代碼:
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)使用詳細介紹》 (https://zhuanlan.zhihu.com/p/72777951)。
2.1 建立 event bus
在 Vue.js 中建立 EventBus 有兩種方式:
- 手動實作,導出 Vue 執行個體化的結果。
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue();
- 直接在項目中的
全局挂載 Vue 執行個體化的結果。main.js
// 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.《釋出/訂閱》(https://zh.wikipedia.org/wiki/釋出/訂閱)
2.《觀察者模式VS訂閱釋出模式》(https://molunerfinn.com/observer-vs-pubsub-pattern/#概述)