從零帶你手寫一個“發佈-訂閲者模式“ ,保姆級教學

語言: 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()
    });
}

} ```

❤️ 結尾

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

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

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

更多更全更詳細優質內容猛戳這裏查看

在這裏插入圖片描述