實現一個完整的發布訂閱模式
前言
發布-訂閱模式是我們經常會接觸的到的設計模式,它定義一個對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。在 JavaScript 開發中,我們一般用事件模型來替代傳統的發布-訂閱模式
現實生活中的發布訂閱模式
毛小星是一個球鞋愛好者,他最近在逛淘寶的時候,看上了一雙芝加哥配色的 AJ One 籃球鞋,但是問過客服美眉被告知,這個配色的鞋已經售罄了。好在客服美眉告訴毛小星,不久后耐克公司還會復刻一批芝加哥配色的球鞋,但是具體什么時候,目前還沒有確切消息。
於是客服美眉告訴毛小星,只要收藏我們的店鋪,一旦有新球鞋進貨的時候,就立馬發出消息通知毛小星。此時楊小A也想買一雙球鞋,於是也按照客服美眉的建議收藏了店鋪,耐心的等待球鞋進貨的消息。
上面這個例子中,球鞋到貨,自動發送消息通知就是一個典型的發布-訂閱模式,毛小星、楊小A這兩個購買者就是訂閱者,他們收藏了店鋪,店鋪就作為發布者,在球鞋到貨的時候就會給遍歷收藏店鋪的訂閱者,然后給它們發送消息。
其實還有很多發布-訂閱模式的應用場景,例如 Vue 的 EventBus,微前端中的兩個子應用之間的通信。
發布-訂閱模式的優點
-
- 支持簡單的廣播通信,當對象狀態發生改變時,會自動通知已經訂閱過的對象。買鞋的人不用再給天天給客服美眉發送消息了,一旦有鞋子,店鋪會自動通知買家。
這個特性被廣泛應用於異步編程中,這是一種替代傳遞回調函數的方案。比如,我們可以訂閱 ajax 請求的 error、success等事件。或者如果想在動畫的每一幀完成之后做一些事情,那我們可以訂閱一個事件,然后在動畫的每一幀完成之后發布這個事件。在異步編程中使用發布-訂閱模式,我們就無需過多關注對象在異步運行期間的內部狀態,而只需要訂閱感興趣的事件發生點。
- 發布者與訂閱者耦合性降低,發布者只管發布一條消息出去,它不關心這條消息如何被訂閱者使用,同時,訂閱者只監聽發布者的事件名,只要發布者的事件名不變,它不管發布者如何改變;同理賣家(發布者)它只需要將鞋子來貨的這件事告訴訂閱者(買家),他不管買家到底買還是不買,還是買其他賣家的。只要鞋子到貨了就通知訂閱者即可。
第二點說明發布-訂閱模式可以去到對象之間的硬編碼的通知機制,一個對象不用再顯示的調用另外一個對象的某個接口。發布-訂閱模式讓兩個對象松耦合得聯系在一起,雖然不太清楚彼此的細節,但這不影響他們之間互相通信。當有新的訂閱者出現時,發布者的代碼不需要任何修改;同樣發布者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。
發布-訂閱模式的缺點
- 創建訂閱者需要消耗一定的時間和內存。
- 雖然可以弱化對象之間的練習,但是如果過度使用的話,代碼反而不太好理解和維護。特別是有多個發布者和訂閱者的嵌套到一起的時候,要跟蹤一個bug並不是一件輕松的事情。
實現一個通用的發布訂閱模式
話不多說,直接上代碼,不上代碼的技術博客都是耍流氓!
// 定義事件中心類
class MyEvent {
handlers = {} // 存放事件 map,發布者,存放訂閱者
$on(type, fn) {
if (!Reflect.has(this.handlers, type)) { // 如果沒有定義過該事件,初始化該訂閱者列表
this.handlers[type] = []
}
this.handlers[type].push(fn) // 存放訂閱的消息
}
$emit(type, ...params) {
if (!Reflect.has(this.handlers, type)) { // 如果沒有該事件,拋出錯誤
throw new Error(`未注冊該事件${type}`)
}
this.handlers[type].forEach((fn) => { // 循環事件列表,執行每一個事件,相當於向訂閱者發送消息
fn(...params)
})
}
$remove(type, fn) {
if (!Reflect.has(this.handlers, type)) {
throw new Error(`無效事件${type}`)
}
if (!fn) { // 如果沒有傳入方法,表示需要將該該類型的所有消息取消訂閱
return Reflect.deleteProperty(this.handlers, type)
} else {
const inx = this.handlers[type].findIndex((handler) => handler === fn)
if (inx === -1) { // 如果該事件不在事件列表中,則拋出錯誤
throw new Error('無效事件')
}
this.handlers[type].splice(inx, 1) // 從事件列表中刪除該事件
if (!this.handlers[type].length) { // 如果該類事件列表中沒有事件了,則刪除該類事件
return Reflect.deleteProperty(this.handlers, type)
}
}
}
}