從零帶你手寫一個“釋出-訂閱者模式“ ,保姆級教學
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. 新增三個核心方法
還需要新增三個方法,也就是我們前面講到的on
、emit
和off
方法,為了讓這個方法長得更像 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監聽一下
買紅寶石,紅寶石到了之後,執行回撥函式
handlerA和
handlerB`,就可以這樣寫:
```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
型別的內容,完成之後執行回撥函式 handlerA
和 handlerB
``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]
}
即每個事件型別對應多個訊息 (回撥函式),這樣的話我們就要為每個事件型別建立一個數組,具體寫法:
- 先判斷有沒有這個屬性(事件型別)
- 如果沒有這個屬性,就初始化一個空的陣列
- 如果有這個屬性,就往他的後面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
``` 輸出結果:
打印出的 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
方法也需要type
和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) {}
// 觸發訊息佇列裡的內容
$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操作刪除的。
-
任何用
let
或const
宣告的屬性不能夠從它被宣告的作用域中刪除。 -
不可設定的(Non-configurable)屬性不能被移除。這意味著像
Math
,Array
,Object
內建物件的屬性以及使用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()
});
}
} ```
❤️ 結尾
如果這篇文章 對你的學習 有所 幫助,歡迎 點贊 👍 收藏 ⭐ 留言 📝 ,你的支援 是我 創作分享 的 動力!
學習過程中如果有疑問,點選這裡,可以獲得我的聯絡方式,與我交流~
關注公眾號「前端圓圓」,第一時間獲取文章更新。
更多更全更詳細 的 優質內容, 猛戳這裡檢視