TypeScript 設計模式之發佈-訂閱模式

前言

在以前兩篇自測清單中,和你們分享了不少 JavaScript 基礎知識,你們能夠一塊兒再回顧下~javascript

本文是我在咱們團隊內部「現代 JavaScript 突擊隊」分享的一篇內容,第二期學習內容爲「設計模式」系列,我會將我負責分享的知識整理成文章輸出,但願可以和你們一塊兒溫故知新!php

現代 JavaScript 突擊隊」學習總結:前端

  1. 《初中級前端 JavaScript 自測清單 - 1》
  2. 《初中級前端 JavaScript 自測清單 - 2》
  3. 《TypeScript 設計模式之觀察者模式》
  4. 《TypeScript語法總結+項目(Vue.js+TS)實戰》

1、模式介紹

1. 生活場景

最近剛畢業的學生 Leo 準備開始租房了,他來到房產中介,跟中介描述了本身的租房需求,開開心心回家了。次日,中介的小哥哥小姐姐爲 Leo 列出符他需求的房間,並打電話約他一塊兒看房了,最後 Leo 選中一套滿意的房間,高高興興過去籤合同,準備開始新生活~vue

還有個大佬 Paul,準備將手中 10 套房出租出去,因而他來到房產中介,在中介那邊提供了本身要出租的房間信息,溝通好手續費,開開心心回家了。次日,Paul 接到中介的好消息,房子租出去了,因而他高高興興過去籤合同,開始收房租了~java

發佈-訂閱模式(簡介).png

上面場景有個須要特別注意的地方:typescript

  • 租戶在租房過程當中,不知道房間具體房東是誰,到後面籤合同才知道;
  • 房東在出租過程當中,不知道房間具體租戶是誰,到後面籤合同才知道;

這兩點其實就是後面要介紹的 發佈-訂閱模式 的一個核心特色。編程

2. 概念介紹

軟件架構中,發佈-訂閱模式是一種消息範式,消息的發送者(稱爲發佈者)不會將消息直接發送給特定的接收者(稱爲訂閱者)。而是將發佈的消息分爲不一樣的類別,無需瞭解哪些訂閱者(若是有的話)可能存在。一樣的,訂閱者能夠表達對一個或多個類別的興趣,只接收感興趣的消息,無需瞭解哪些發佈者(若是有的話)存在。設計模式

發佈-訂閱是消息隊列範式的兄弟,一般是更大的面向消息中間件系統的一部分。大多數消息系統在API中同時支持消息隊列模型和發佈/訂閱模型,例如Java消息服務(JMS)。微信

這種模式提供了更大的網絡可擴展性和更動態的網絡拓撲,同時也下降了對發佈者和發佈數據的結構修改的靈活性。網絡

2、 觀察者模式 vs 發佈-訂閱模式

看完上面概念,有沒有以爲與觀察者模式很像?
但其實二者仍是有差別的,接下來一塊兒看看。

1. 概念對比

咱們分別爲經過兩種實際生活場景來介紹這兩種模式:

  • 觀察者模式:如微信中 顧客-微商 關係;
  • 發佈-訂閱模式:如淘寶購物中 顧客-淘寶-商家 關係。

這兩種場景的過程分別是這樣:

1.1 觀察者模式

觀察者模式(微商與顧客).png
觀察者模式中,消費顧客關注(如加微信好友)本身有興趣的微商,微商就會私聊發本身在賣的產品給消費顧客。
這個過程當中,消費顧客至關於觀察者(Observer),微商至關於觀察目標(Subject)。

1.2 發佈-訂閱模式

接下來看看 發佈-訂閱模式 :

發佈-訂閱模式(淘寶與顧客) .png
發佈-訂閱模式 中,消費顧客經過淘寶搜索本身關注的產品,商家經過淘寶發佈商品,當消費顧客在淘寶搜索的產品,已經有商家發佈,則淘寶會將對應商品推薦給消費顧客。
這個過程當中,消費顧客至關於訂閱者,淘寶至關於事件總線,商家至關於發佈者。

2. 流程對比

觀察者模式和發佈-訂閱模式區別(流程圖).png

3. 小結

因此能夠看出,觀察者模式發佈-訂閱模式差異在於有沒有一箇中央的事件總線。若是有,咱們就能夠認爲這是個發佈-訂閱模式。若是沒有,那麼就能夠認爲是觀察者模式。由於其實它們都實現了一個關鍵的功能:發佈事件-訂閱事件並觸發事件

3、模式特色

對比完觀察者模式發佈-訂閱模式後,咱們大體理解發佈-訂閱模式是什麼了。接着總結下該模式的特色:

1. 模式組成

在發佈-訂閱模式中,一般包含如下角色:

  • 發佈者:Publisher
  • 事件總線:Event Channel
  • 訂閱者:Subscriber

2. UML 類圖

發佈-訂閱模式(UML).jpg

3. 優勢

  1. 鬆耦合(Independence)

發佈-訂閱模式能夠將衆多須要通訊的子系統(Subsystem)解耦,每一個子系統獨立管理。並且即便部分子系統取消訂閱,也不會影響事件總線的總體管理。
發佈-訂閱模式中每一個應用程序均可以專一於其核心功能,而事件總線負責將消息路由到每一個訂閱者手裏。

  1. 高伸縮性(Scalability)

發佈-訂閱模式增長了系統的可伸縮性,提升了發佈者的響應能力。緣由是發佈者(Publisher)能夠快速地向輸入通道發送一條消息,而後返回到其核心處理職責,而沒必要等待子系統處理完成。而後事件總線負責確保把消息傳遞到每一個訂閱者(Subscriber)手裏。

  1. 高可靠性(Reliability)

發佈-訂閱模式提升了可靠性。異步的消息傳遞有助於應用程序在增長的負載下繼續平穩運行,而且能夠更有效地處理間歇性故障。

  1. 靈活性(Flexibility)

你不須要關心不一樣的組件是如何組合在一塊兒的,只要他們共同遵照一份協議便可。
發佈-訂閱模式容許延遲處理或者按計劃的處理。例如當系統負載大的時候,訂閱者能夠等到非高峯時間才接收消息,或者根據特定的計劃處理消息。

4. 缺點

  1. 在建立訂閱者自己會消耗內存,但當訂閱消息後,沒有進行發佈,而訂閱者會一直保存在內存中,佔用內存;
  2. 建立訂閱者須要消耗必定的時間和內存。若是過分使用的話,反而使代碼很差理解及代碼很差維護。

4、使用場景

若是咱們項目中不多使用到訂閱者,或者與子系統實時交互較少,則不適合 發佈-訂閱模式 。
在如下狀況下能夠考慮使用此模式:

  1. 應用程序須要向大量消費者廣播信息。例如微信訂閱號就是一個消費者量龐大的廣播平臺。
  2. 應用程序須要與一個或多個獨立開發的應用程序或服務通訊,這些應用程序或服務可能使用不一樣的平臺、編程語言和通訊協議。
  3. 應用程序能夠向消費者發送信息,而不須要消費者的實時響應。

5、實戰示例

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', {})

6、總結

觀察者模式和發佈-訂閱模式的差異在於事件總線,若是有則是發佈-訂閱模式,反之爲觀察者模式。因此在實現發佈-訂閱模式,關鍵在於實現這個事件總線,在某個特定時間觸發某個特定事件,從而觸發監聽這個特定事件的組件進行相應操做的功能。發佈-訂閱模式在不少時候很是有用。

參考文章

1.《發佈/訂閱》 
2.《觀察者模式VS訂閱發佈模式》 

相關文章
相關標籤/搜索