從零帶你手寫一個“釋出-訂閱者模式“ ,保姆級教學

語言: CN / TW / HK
ead>

前言

釋出-訂閱模式其實是一種物件間一對多的依賴關係,當一個物件的狀態傳送改變時,所有依賴於它的物件都將得到狀態改變的通知。

  • 訂閱者(Subscriber)把自己想訂閱的事件 註冊(Subscribe)到排程中心(Event Channel);
  • 釋出者(Publisher)釋出該事件(Publish Event)到排程中心,也就是該事件觸發時,由 排程中心 統一排程(Fire Event)訂閱者註冊到排程中心的處理程式碼。

◾ 例子

比如我們很喜歡看某個公眾號的文章,但是不知道什麼時候釋出新文章,要不定時的去翻閱;這時候,我們可以關注該公眾號,當有文章推送時,會有訊息及時通知我們文章更新了。

上面一個看似簡單的操作,其實是一個典型的釋出訂閱模式,公眾號屬於釋出者,使用者屬於訂閱者;使用者將訂閱公眾號的事件註冊到排程中心,公眾號作為釋出者,當有新文章釋出時,公眾號釋出該事件到排程中心,排程中心會及時發訊息告知使用者。

◾ 釋出/訂閱模式的優點是物件之間解耦,非同步程式設計中,可以更鬆耦合的程式碼編寫;缺點是建立訂閱者本身要消耗一定的時間和記憶體,雖然可以弱化物件之間的聯絡,多個釋出者和訂閱者巢狀一起的時候,程式難以跟蹤維護。

手寫實現釋出-訂閱者模式

整體的釋出-訂閱者模式實現思路如下: - 建立一個類 class - 在這個類裡建立一個快取列表(排程中心) - on 方法 - 用來把函式fn新增到快取列表(訂閱者註冊事件到排程中心) - emit 方法 - 取到event事件型別,根據event值去執行對應快取列表中的函式(釋出者釋出事件到排程中心,排程中心處理程式碼) - off 方法 - 可以根據event事件型別取消訂閱(取消訂閱)

接下來我們根據上面的思路,開始手寫釋出-訂閱者模式 👇

1. 建立一個 Observer 類

我們先建立一個 Ovserver 類: javascript + class Observer { + + } 在 Observer 類裡,需要新增一個建構函式:

javascript class Observer { + constructor(){ + + } }

2. 新增三個核心方法

還需要新增三個方法,也就是我們前面講到的onemitoff方法,為了讓這個方法長得更像 Vue,我們在這幾個方法前面都加上$,即:

  • 向訊息佇列新增內容 $on
  • 刪除訊息佇列裡的內容 $off
  • 觸發訊息佇列裡的內容 $emit

```javascript class Observer { constructor() {

}
  • // 向訊息佇列新增內容 $on
  • $on(){}
  • // 刪除訊息佇列裡的內容 $off
  • $off(){}
  • // 觸發訊息佇列裡的內容 $emit
  • $emit(){} } ```

方法具體的內容我們放一放,先來建立一個訂閱者(釋出者),

使用建構函式建立一個例項:

```javascript class Observer { constructor() {

}
// 向訊息佇列新增內容 `$on`
$on(){}
// 刪除訊息佇列裡的內容 `$off`
$off(){}
// 觸發訊息佇列裡的內容 `$emit`
$emit(){}

}

  • // 使用建構函式建立一個例項
  • const person1 = new Observer() `` 接著,我們向這個person1委託一些內容,也就是說呼叫person1 $ON`方法:

```javascript class Observer { constructor() {

}
// 向訊息佇列新增內容 `$on`
$on() {}
// 刪除訊息佇列裡的內容 `$off`
$off() {}
// 觸發訊息佇列裡的內容 `$emit`
$emit() {}

}

// 使用建構函式建立一個例項 const person1 = new Observer();

  • // 向這個person1委託一些內容,呼叫person1$ON方法
  • person1.$on() ``` 既然要委託一些內容,那 事件名 就必不可少,事件觸發的時候也需要一個 回撥函式

  • 事件名

  • 回撥函式

舉個例子,我們寫幾個事件,比如:

```javascript class Observer { constructor() {

}
// 向訊息佇列新增內容 `$on`
$on() {}
// 刪除訊息佇列裡的內容 `$off`
$off() {}
// 觸發訊息佇列裡的內容 `$emit`
$emit() {}

}

// 使用建構函式建立一個例項 const person1 = new Observer();

// 向這個person1委託一些內容,呼叫person1$ON方法 person1.$on()

  • function handlerA() {
  • console.log('handlerA');
  • }
  • function handlerB() {
  • console.log('handlerB');
  • }
  • function handlerC() {
  • console.log('handlerC'); } `` 我們現在拜託person1監聽一下買紅寶石,紅寶石到了之後,執行回撥函式handlerAhandlerB`,就可以這樣寫:

```javascript class Observer { constructor() {

}
// 向訊息佇列新增內容 `$on`
$on() {}
// 刪除訊息佇列裡的內容 `$off`
$off() {}
// 觸發訊息佇列裡的內容 `$emit`
$emit() {}

}

// 使用建構函式建立一個例項 const person1 = new Observer();

// 向這個person1委託一些內容,呼叫person1$ON方法 + person1.$on('買紅寶石', handlerA) + person1.$on('買紅寶石', handlerB)

function handlerA() { console.log('handlerA'); }

function handlerB() { console.log('handlerB'); }

function handlerC() { console.log('handlerC'); } ```

再拜託 person1 監聽一下 買奶茶 ,奶茶到了之後,執行回撥函式 handlerC :

```javascript class Observer { constructor() {

}
// 向訊息佇列新增內容 `$on`
$on() {}
// 刪除訊息佇列裡的內容 `$off`
$off() {}
// 觸發訊息佇列裡的內容 `$emit`
$emit() {}

}

// 使用建構函式建立一個例項 const person1 = new Observer();

// 向這個person1委託一些內容,呼叫person1$ON方法 person1.$on('買紅寶石', handlerA) person1.$on('買紅寶石', handlerB)

  • person1.$on('買奶茶', handlerC)

function handlerA() { console.log('handlerA'); }

function handlerB() { console.log('handlerB'); }

function handlerC() { console.log('handlerC'); } ```

3. 設定快取列表

到這裡我們就需要前面講到的 快取列表(訊息佇列),也就是排程中心了。

Observer類新增 快取列表:

``javascript class Observer { constructor() { + this.message = {} // 訊息佇列 } // 向訊息佇列新增內容$on$on() {} // 刪除訊息佇列裡的內容$off$off() {} // 觸發訊息佇列裡的內容$emit` $emit() {} }

// 使用建構函式建立一個例項 const person1 = new Observer(); `` 這個快取列表message` 物件的功能如下:

person1 委託一個buy型別的內容,完成之後執行回撥函式 handlerAhandlerB

``javascript class Observer { constructor() { this.message = {} // 訊息佇列 } // 向訊息佇列新增內容$on$on() {} // 刪除訊息佇列裡的內容$off$off() {} // 觸發訊息佇列裡的內容$emit` $emit() {} }

// 使用建構函式建立一個例項 const person1 = new Observer();

  • person1.$on('buy',handlerA);
  • person1.$on('buy',handlerB);

function handlerA() { console.log('handlerA'); }

function handlerB() { console.log('handlerB'); }

function handlerC() { console.log('handlerC'); } `` 我們希望通過$on向訊息佇列新增上面內容後,就相當對給message物件添加了一個buy屬性,這個屬性值為[handlerA, handlerB]`,相當於下面的效果:

javascript class Observer { constructor() { this.message = { + buy: [handlerA, handlerB] } } // 向訊息佇列新增內容 `$on` $on() {} // 刪除訊息佇列裡的內容 `$off` $off() {} // 觸發訊息佇列裡的內容 `$emit` $emit() {} } 需求明確後,下面著手 $on 函式 👇👇👇

4. 實現 $on 方法

回顧一行程式碼:

javascript person1.$on('buy',handlerA); 很明顯我們給$on方法傳入了兩個引數:

  • type:事件名 (事件型別)
  • callback:回撥函式

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

  • /**
    • $on 向訊息佇列新增內容
    • @param {*} type 事件名 (事件型別)
    • @param {*} callback 回撥函式
  • */
  • $on(type, callback) {} // 刪除訊息佇列裡的內容 $off $off() {} // 觸發訊息佇列裡的內容 $emit $emit() {} }

// 使用建構函式建立一個例項 const person1 = new Observer();

person1.$on('buy', handlerA); person1.$on('buy', handlerB); ``` 我們初步設想一下如何向訊息佇列新增內容,訊息佇列是一個物件,可以通過下面的方法新增事件內容:

```js class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
  • this.message[type] = callback; } // 刪除訊息佇列裡的內容 $off $off() {} // 觸發訊息佇列裡的內容 $emit $emit() {} } ``` 但通過前文我們知道訊息佇列中每個屬性值都是 陣列

js this.message = { buy: [handlerA, handlerB] }

即每個事件型別對應多個訊息 (回撥函式),這樣的話我們就要為每個事件型別建立一個數組,具體寫法:

  1. 先判斷有沒有這個屬性(事件型別)
  2. 如果沒有這個屬性,就初始化一個空的陣列
  3. 如果有這個屬性,就往他的後面push一個新的 callback

程式碼實現如下:

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
  • // 判斷有沒有這個屬性(事件型別)
  • if (!this.message[type]) {
  • // 如果沒有這個屬性,就初始化一個空的陣列
  • this.message[type] = [];
  • }
  • // 如果有這個屬性,就往他的後面push一個新的callback
  • this.message[type].push(callback) } // 刪除訊息佇列裡的內容 $off $off() {} // 觸發訊息佇列裡的內容 $emit $emit() {} } ``$on` 的程式碼實現如上所示,我們加上用例並引入到一個html檔案中測試一下:

Observe.js

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
    // 判斷有沒有這個屬性(事件型別)
    if (!this.message[type]) {
        // 如果沒有這個屬性,就初始化一個空的陣列
        this.message[type] = [];
    }
    // 如果有這個屬性,就往他的後面push一個新的callback
    this.message[type].push(callback)
}
// 刪除訊息佇列裡的內容 `$off`
$off() {}
// 觸發訊息佇列裡的內容 `$emit`
$emit() {}

}

function handlerA() { console.log('handlerA'); } function handlerB() { console.log('handlerB'); } function handlerC() { console.log('handlerC'); }

// 使用建構函式建立一個例項 const person1 = new Observer();

person1.$on('buy', handlerA); person1.$on('buy', handlerB);

console.log('person1 :>> ', person1); ```

Oberver.html

```html

Document

``` 輸出結果:

在這裡插入圖片描述

打印出的 person1 是 Oberver 型別的,裡面有一個message,也就是咱定義的訊息佇列;這個message裡有我們新增的buy型別的事件,這個buy事件有兩個訊息:[handlerA,handlerB],測試通過 👏👏👏

接下來,我們來實現 $off 方法

5. 實現 $off 方法

$off 方法用來刪除訊息佇列裡的內容

$off 方法有兩種寫法: 1. person1.$off("buy") - 刪除整個buy事件型別 2. person1.$off("buy",handlerA) - 只刪除handlerA訊息,保留buy事件列表裡的其他訊息

$on方法一樣,$off方法也需要typecallback這兩個方法:

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
    // 判斷有沒有這個屬性(事件型別)
    if (!this.message[type]) {
        // 如果沒有這個屬性,就初始化一個空的陣列
        this.message[type] = [];
    }
    // 如果有這個屬性,就往他的後面push一個新的callback
    this.message[type].push(callback)
}
  • /**
    • $off 刪除訊息佇列裡的內容
    • @param {*} type 事件名 (事件型別)
    • @param {*} callback 回撥函式
  • */
  • $off(type, callback) {}

    // 觸發訊息佇列裡的內容 $emit $emit() {} } ``$off`方法的實現步驟如下:

  • 判斷是否有訂閱,即訊息佇列裡是否有type這個型別的事件,沒有的話就直接return

  • 判斷是否有fn這個引數
  • 沒有fn就刪掉整個事件
  • 有fn就僅僅刪掉fn這個訊息

程式碼實現如下:

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
    // 判斷有沒有這個屬性(事件型別)
    if (!this.message[type]) {
        // 如果沒有這個屬性,就初始化一個空的陣列
        this.message[type] = [];
    }
    // 如果有這個屬性,就往他的後面push一個新的callback
    this.message[type].push(callback);
}

/**
 * $off 刪除訊息佇列裡的內容
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$off(type, callback) {
  • // 判斷是否有訂閱,即訊息佇列裡是否有type這個型別的事件,沒有的話就直接return
  • if (!this.message[type]) return;
  • // 判斷是否有callback這個引數
  • if (!callback) {
  • // 如果沒有callback,就刪掉整個事件ß
  • this.message[type] = undefined;
  • }
  • // 如果有callback,就僅僅刪掉callback這個訊息(過濾掉這個訊息方法)
  • this.message[type] = this.message[type].filter((item) => item !== callback); }

    // 觸發訊息佇列裡的內容 $emit $emit() {} } `` 以上就是$off`的實現,我們先來測試一下:

```javascript class Observer { ... }

function handlerA() { console.log('handlerA'); } function handlerB() { console.log('handlerB'); } function handlerC() { console.log('handlerC'); }

// 使用建構函式建立一個例項 const person1 = new Observer();

person1.$on('buy', handlerA); person1.$on('buy', handlerB); person1.$on('buy', handlerC);

console.log('person1 :>> ', person1); ``` 輸出結果:

在這裡插入圖片描述

● 測試刪除單個訊息,使用$off 刪除 handlerC 訊息

```javascript class Observer { ... }

function handlerA() { console.log('handlerA'); } function handlerB() { console.log('handlerB'); } function handlerC() { console.log('handlerC'); }

// 使用建構函式建立一個例項 const person1 = new Observer();

person1.$on('buy', handlerA); person1.$on('buy', handlerB); person1.$on('buy', handlerC);

console.log('person1 :>> ', person1);

  • // 刪除 handlerC 訊息
  • person1.$off('buy',handlerC);

  • console.log('person1 :>> ', person1); ``` 輸出結果:

在這裡插入圖片描述

測試通過 🥳🥳🥳

● 測試刪除整個事件型別,使用$off 刪除整個 buy 事件

```javascript class Observer { ... }

function handlerA() { console.log('handlerA'); } function handlerB() { console.log('handlerB'); } function handlerC() { console.log('handlerC'); }

// 使用建構函式建立一個例項 const person1 = new Observer();

person1.$on('buy', handlerA); person1.$on('buy', handlerB); person1.$on('buy', handlerC);

console.log('person1 :>> ', person1);

// 刪除 handlerC 訊息 person1.$off('buy',handlerC);

console.log('person1 :>> ', person1);

  • // 刪除 buy 事件
  • person1.$off('buy');

  • console.log('person1 :>> ', person1); ``` 輸出結果:

在這裡插入圖片描述

Perfect!!!測試通過 ✅

這樣以來 $off 的兩個功能我們就已經成功實現 👏👏👏

● 關於 $off 的實現,這裡講一個小細節 👇

javascript 刪除物件的某個屬性 有兩種方法: 1. delete 操作符 2. obj.key = undefined; (等同於obj[key] = undefined;)

這兩種方法的區別:

1. delete 操作符會從某個物件上移除指定屬性,但它的工作量比其“替代”設定也就是 object[key] = undefined 多的多的多。

並且該方法有諸多限制,比如,以下情況需要重點考慮:

  • 如果你試圖刪除的屬性不存在,那麼delete將不會起任何作用,但仍會返回true

  • 如果物件的原型鏈上有一個與待刪除屬性同名的屬性,那麼刪除屬性之後,物件會使用原型鏈上的那個屬性(也就是說,delete操作只會在自身的屬性上起作用)

  • 任何使用 var 宣告的屬性不能從全域性作用域或函式的作用域中刪除。

    • 這樣的話,delete操作不能刪除任何在全域性作用域中的函式(無論這個函式是來自於函式宣告或函式表示式)
    • 除了在全域性作用域中的函式不能被刪除,在物件(object)中的函式是能夠用delete操作刪除的。
  • 任何用letconst 宣告的屬性不能夠從它被宣告的作用域中刪除。

  • 不可設定的(Non-configurable)屬性不能被移除。這意味著像Math, ArrayObject內建物件的屬性以及使用Object.defineProperty()方法設定為不可設定的屬性不能被刪除。

2. obj[key] = undefined; 這個選擇不是這個問題的正確答案,因為只是把某個屬性替換為undefined,屬性本身還在。但是,如果你小心使用它,你可以大大加快一些演算法。

好了,回到正題 🙌

接下來我們開始實現第三個方法 $emit 🧗‍♀️

6. 實現 $emit 方法

$emit 用來觸發訊息佇列裡的內容:

  • 該方法需要傳入一個 type 引數,用來確定觸發哪一個事件;

  • 主要流程就是對這個type事件做一個輪詢 (for迴圈),挨個執行每一個訊息的回撥函式callback就👌了。

具體程式碼實現如下:

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
    // 判斷有沒有這個屬性(事件型別)
    if (!this.message[type]) {
        // 如果沒有這個屬性,就初始化一個空的陣列
        this.message[type] = [];
    }
    // 如果有這個屬性,就往他的後面push一個新的callback
    this.message[type].push(callback);
}

/**
 * $off 刪除訊息佇列裡的內容
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$off(type, callback) {
    // 判斷是否有訂閱,即訊息佇列裡是否有type這個型別的事件,沒有的話就直接return
    if (!this.message[type]) return;
    // 判斷是否有callback這個引數
    if (!callback) {
        // 如果沒有callback,就刪掉整個事件
        this.message[type] = undefined;
        return;
    }
    // 如果有callback,就僅僅刪掉callback這個訊息(過濾掉這個訊息方法)
    this.message[type] = this.message[type].filter((item) => item !== callback);
}
  • /**
    • $emit 觸發訊息佇列裡的內容
    • @param {*} type 事件名 (事件型別)
  • */
  • $emit(type) {
  • // 判斷是否有訂閱
  • if(!this.message[type]) return;
  • // 如果有訂閱,就對這個type事件做一個輪詢 (for迴圈)
  • this.message[type].forEach(item => {
  • // 挨個執行每一個訊息的回撥函式callback
  • item()
  • });
  • } } ``` 打完收工🏌️‍♀️

來測試一下吧~

```javascript class Observer { ... }

function handlerA() { console.log('buy handlerA'); } function handlerB() { console.log('buy handlerB'); } function handlerC() { console.log('buy handlerC'); }

// 使用建構函式建立一個例項 const person1 = new Observer();

  • person1.$on('buy', handlerA);
  • person1.$on('buy', handlerB);
  • person1.$on('buy', handlerC);

console.log('person1 :>> ', person1);

  • // 觸發 buy 事件
  • person1.$emit('buy') ``` 輸出結果:

在這裡插入圖片描述

測試通過 👏👏👏

在這裡插入圖片描述

完整程式碼

本篇文章實現了最簡單的釋出訂閱者模式,他的核心內容只有四個: 1. 快取列表 message 2. 向訊息佇列新增內容 $on 3. 刪除訊息佇列裡的內容 $off 4. 觸發訊息佇列裡的內容 $emit

釋出訂閱者模式完整程式碼實現:

完整版的程式碼較長,這裡看著如果不方便的可以去我的GitHub上看,我專門維護了一個 前端 BLOG 的倉庫https://github.com/yuanyuanbyte/Blog

```javascript class Observer { constructor() { this.message = {} // 訊息佇列 }

/**
 * `$on` 向訊息佇列新增內容 
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$on(type, callback) {
    // 判斷有沒有這個屬性(事件型別)
    if (!this.message[type]) {
        // 如果沒有這個屬性,就初始化一個空的陣列
        this.message[type] = [];
    }
    // 如果有這個屬性,就往他的後面push一個新的callback
    this.message[type].push(callback);
}

/**
 * $off 刪除訊息佇列裡的內容
 * @param {*} type 事件名 (事件型別)
 * @param {*} callback 回撥函式
 */
$off(type, callback) {
    // 判斷是否有訂閱,即訊息佇列裡是否有type這個型別的事件,沒有的話就直接return
    if (!this.message[type]) return;
    // 判斷是否有callback這個引數
    if (!callback) {
        // 如果沒有callback,就刪掉整個事件
        this.message[type] = undefined;
        return;
    }
    // 如果有callback,就僅僅刪掉callback這個訊息(過濾掉這個訊息方法)
    this.message[type] = this.message[type].filter((item) => item !== callback);
}

/**
 * $emit 觸發訊息佇列裡的內容
 * @param {*} type 事件名 (事件型別)
 */
$emit(type) {
    // 判斷是否有訂閱
    if(!this.message[type]) return;
    // 如果有訂閱,就對這個`type`事件做一個輪詢 (for迴圈)
    this.message[type].forEach(item => {
        // 挨個執行每一個訊息的回撥函式callback
        item()
    });
}

} ```

❤️ 結尾

如果這篇文章 對你的學習 有所 幫助,歡迎 點贊 👍 收藏留言 📝 ,你的支援 是我 創作分享動力!

學習過程中如果有疑問,點選這裡,可以獲得我的聯絡方式,與我交流~

關注公眾號「前端圓圓」,第一時間獲取文章更新。

更多更全更詳細優質內容猛戳這裡檢視

在這裡插入圖片描述