javascript 設計模式之發佈訂閱者模式

文章系列

javascript 設計模式之單例模式javascript

javascript 設計模式之適配器模式java

javascript 設計模式之裝飾者模式node

javascript設計模式之代理模式git

javascript 適配、代理、裝飾者模式的比較github

javascript 設計模式之狀態模式web

javascript 設計模式之迭代器模式面試

javascript 設計模式之策略模式設計模式

javascript 設計模式之觀察者模式promise

javascript 設計模式之發佈訂閱者模式markdown

前言

上一篇講到觀察者模式,這篇要講下發布-訂閱模式。 有人可能會說了:這兩個不是一回事嗎?確實這兩個模式的核心思想、運行機制上沒有本質的差異。 但仍是有些差異,要否則我這篇要講啥,且聽我娓娓道來

本文代碼

什麼是發佈訂閱者模式

基於一個主題/事件通道,但願接收通知的對象(稱爲subscriber)經過自定義事件訂閱主題,被激活事件的對象(稱爲publisher)經過發佈主題事件的方式被通知。

發佈訂閱者模式

生活中的發佈訂閱者模式

還記得上次那個小明、小紅、小二去樓盤買房的事?

小明最近看中了一個樓盤,到了售樓處以後才被告知,該樓盤的房子還未開盤,具體開盤時間還沒定。除了小明,一塊兒來的還有小紅,小二人,爲了第一時間得知樓盤開盤時間,他們都把電話號碼留給了銷售員王五,王五也答應樓盤一開盤就會發消息通知他們。 過了不久,新樓盤開盤時間定了,王五趕忙拿出花名冊,遍歷上面的號碼,羣發了短信通知了他們。 至於這些人收到通知是選擇走路來、開車來、或者不來就自行決定了

這個是觀察者模式。

但若是小明等人沒有去售樓處找五五,而是去該樓盤官網查看,得知還沒開盤關注了該樓盤進展。開盤以後,王五也不會羣發短信,而是將消息同步到官網,這樣官網自動通知了關注該樓盤的意向購房者。 這樣就是發佈訂閱者模式。

重點概念對號入座

發佈者: 王五

訂閱者: 小明、小紅,小二

事件中心: 官網

訂閱事件: 小明、小紅、小二進入官網關注樓盤進展

通知變化: 官網自動通知了關注樓盤的意向購買者

這二者有啥差異呢?

  • 發佈訂閱者模式,發佈者跟訂閱者之間並無直接依賴,都是經過事件中心來處理。
  • 發佈訂閱者模式,全部事件的發佈/訂閱操做,必須經由事件中心,禁止一切「私下交易」

手寫發佈訂閱者模式

類比官網,事件中心應該要有發事件( emit ),監聽事件( on ),還有銷燬事件監聽者( off ),銷燬指定事件的全部監聽者( offAll ),還有隻觸發一次( once )的功能

class EventEmitter {
    constructor() {
        // handleEvents 是一個 map ,用於存儲事件與回調之間的對應關係
        this.handleEvents = {}
    }
    /** * 註冊事件監聽者, 它接受事件類型和回調函數做爲參數 * @param {String} type 事件類型 * @param {Function} callback 回調函數 */
    on(type, callback) {
        // 先檢查一下事件類型有沒有對應的監聽函數隊列
        if (!this.handleEvents[type]) {
            // 若是沒有,那麼首先初始化一個監聽函數隊列, 不然直接 push 會報錯
            this.handleEvents[type] = []
        }
        // 把回調函數推入事件類型的監聽函數隊列裏去
        this.handleEvents[type].push(callback)
    }
    /** * 發佈事件,它接受事件類型和監聽函數入參做爲參數 * @param {String} type 事件類型 * @param {...any} args 參數列表,把emit傳遞的參數賦給回調函數 */
    emit(type, ...args) {
        // 檢查事件類型是否有監聽函數隊列
        if (this.handleEvents[type]) {
            // 若是有,則逐個調用隊列裏的回調函數
            this.handleEvents[type].forEach((callback) => {
                callback(...args)
            })
        }
    }
    /** * 移除某個事件回調隊列裏的指定回調函數 * @param {String} type 事件類型 * @param {Function} callback 回調函數 */
    off(type, callback) {
        const callbacks = this.handleEvents[type]
        const index = callbacks.indexOf(callback)
        // 找到則移除
        if (index !== -1) {
            callbacks.splice(index, 1)
        }
        // 該事件類型對應的回調函數爲空了,則將該對象刪除
        if (callbacks.length === 0) {
            delete this.handleEvents[type]
        }
    }
    /** * 移除某個事件的全部回調函數 * @param {String} type 事件類型 */
    offAll(type) {
        if (this.handleEvents[type]) {
            delete this.handleEvents[type]
        }
    }
    /** * 爲事件註冊單次監聽器 * @param {String} type 事件類型 * @param {Function} callback 回調函數 */
    once(type, callback) {
        // 對回調函數進行包裝,使其執行完畢自動被移除
        const wrapper = (...args) => {
            callback.apply(args)
            this.off(type, wrapper)
        }
        this.on(type, wrapper)
    }
}

複製代碼

測試

// 建立事件管理器實例
const emitter = new EventEmitter()
// 註冊一個refresh事件監聽者
emitter.on('refresh', function () {
	console.log('調用後臺獲取最新數據')
})
// 發佈事件refresh
emitter.emit('refresh')
// 也能夠emit傳遞參數
emitter.on('refresh', function (pageNo, pageSize) {
    console.log(`調用後臺獲取參數,參數爲:{pageNo:${pageNo},pageSize:${pageSize}}`)
})
// 此時會打印兩條信息,由於前面註冊了兩個refresh事件的監聽者
emitter.emit('refresh', '2', '20') 

// 測試移除事件監聽
const toBeRemovedListener = function () {
    console.log('我是一個能夠被移除的監聽者')
}
emitter.on('testRemove', toBeRemovedListener)
emitter.emit('testRemove')
emitter.off('testRemove', toBeRemovedListener)
// 此時事件監聽已經被移除,不會再有console.log打印出來了
emitter.emit('testRemove') 

// 測試移除refresh的全部事件監聽
emitter.offAll('refresh')
// 此時能夠看到emitter.handleEvents已經變成空對象了,再emit發送refresh事件也不會有反應了
console.log(emitter) 

emitter.once('onlyOne', function () {
    console.info('只會觸發一次')
})
emitter.emit('onlyOne')
// 不會彈出信息
emitter.emit('onlyOne') 
複製代碼

發佈訂閱模式使用場景

事件監聽

// 訂閱這些函數,當點擊時觸發發佈,會依次觸發這些函數
$('#btn1').click(function () {
    alert(1)
})
$('#btn1').click(function () {
    alert(2)
})
$('#btn1').click(function () {
    alert(3)
})
複製代碼

promise

promise 就是觀察者模式的經典場景: 先將 then 裏面的函數儲存起來,在 resovle 與 reject 裏取出函數,並執行函數。 其實就是收集依賴->觸發通知->取出依賴執行的方式 具體能夠看手寫 promise

jQuery.Callbacks

// 自定義事件,自定義回調
var callbacks = $.Callbacks() // 注意大小寫, C 爲大寫
callbacks.add(function (info) {
    console.info('fn1', info)
})
callbacks.add(function (info) {
    console.info('fn2', info)
})
callbacks.add(function (info) {
    console.info('fn2', info)
})
callbacks.fire('come')
callbacks.fire('go')
複製代碼

node 裏的 event 事件

const EventEmitter = require('events').EventEmitter
const emitter1 = new EventEmitter()
emitter1.on('some', (name) => {
    // 監聽 some 事件
    console.info('some event is occured 1 ' + name)
})
emitter1.on('some', () => {
    // 監聽 some 事件
    console.info('some event is occured 2')
})
// 觸發 some 事件
// 並傳遞參數
emitter1.emit('some', 'zhangsan')
複製代碼

總結

觀察者模式:

  • 角色很明確,沒有事件調度中心做爲中間者,目標對象 Subject 和觀察者 Observer 都要實現約定的成員方法。
  • 雙方聯繫更緊密,目標對象的主動性很強,本身收集和維護觀察者,並在狀態變化時主動通知觀察者更新。

發佈訂閱者模式:

  • 發佈訂閱模式中,發佈者不直接觸及到訂閱者、而是由統一的第三方來完成實際的通訊的操做,互不關心對方是誰。
  • 鬆散耦合,靈活度高,經常使用做事件總線

看的出來,發佈訂閱者模式更加高級,由於它更加鬆散,沒有耦合,那是否是現實中發佈訂閱者模式用的更多呢?實際上不是的。由於在實際開發中,咱們的模塊解耦訴求並非要求它們徹底解耦,若是兩個模塊之間自己存在關聯,且這種關聯是穩定的、必要的,那這時就應該用觀察者模式便可。

參考連接

JavaScript 設計模式核⼼原理與應⽤實踐

從一道面試題簡單談談發佈訂閱和觀察者模式

結語

你的點贊是對我最大的確定,若是以爲有幫助,請留下你的讚揚,謝謝!!!

相關文章
相關標籤/搜索