JavaScript設(shè)計(jì)模式之發(fā)布-訂閱模式

發(fā)布-訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個(gè)對象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對象都將得到通知

1. 現(xiàn)實(shí)中的發(fā)布-訂閱模式

小明最近看上一套房子,到了售樓處之后才被告知,樓盤房子早已售空。好在售樓處告訴小明,不久后還有一些尾盤推出,但到底是什么時(shí)候,目前還不知道。
于是小明記下了售樓處的電話,每天都會打電話詢問有沒有開盤。同樣,除了小明,還有小紅、小強(qiáng)也每天都會像售樓處打電話。一個(gè)星期后,售樓處電話已厭倦了每天的電話回答相同的內(nèi)容。
當(dāng)然,現(xiàn)實(shí)中顯然不是這樣的,實(shí)際上是:售樓處會把意向的客戶的手機(jī)號碼留在售樓處,也就是小紅,小明,小強(qiáng)的電話,當(dāng)開盤時(shí),會依次發(fā)送短信來通知他們。

2. 發(fā)布-訂閱模式的作用

在剛剛的例子中,發(fā)短信通知就是一個(gè)典型的發(fā)布-訂閱模式。小明,小紅他們都是訂閱者,他們訂閱了房子開盤的消息。售樓處作為發(fā)布者,會在合適的時(shí)候依次發(fā)送給所有訂閱者發(fā)送消息。
優(yōu)點(diǎn):

  • 購房者不用天天打電話給售樓處,在合適的時(shí)間,售樓處會作為發(fā)布者通知給所有的訂閱者
  • 購房者和售樓處之前不再強(qiáng)耦合的在一起,當(dāng)有新的購房者出現(xiàn)時(shí),只需要留下手機(jī)號碼,售樓處不需要關(guān)系購房者的任何情況,只需要知道誰訂閱了購房消息即可。
    第一條說明發(fā)布訂閱模式可以廣泛引用于異步編程中,這是一種替代傳遞回調(diào)函數(shù)的方案。比如,訂閱ajax請求的success,error事件。或者如果想在動畫的每一幀完成之后做一些事情,那我們可以訂閱一個(gè)事件,然后在動畫的每一幀完成之后去發(fā)布這個(gè)事件。在異步變成中使用發(fā)布-訂閱模式,我們就無序過多關(guān)注對象在異步運(yùn)行中的內(nèi)部狀態(tài),而只需要訂閱感興趣的事件發(fā)生點(diǎn)。
    第二條說明發(fā)布-訂閱模式可以取代對象之間硬編碼的通知機(jī)制,一個(gè)對象不再顯試的嗲用另外一個(gè)對象的某個(gè)接口。發(fā)布-訂閱模式讓兩個(gè)對象松耦合的聯(lián)系在一起,雖然不太清除彼此的細(xì)節(jié),但不影響他們互相通信。當(dāng)有新的訂閱者出現(xiàn),發(fā)布者的代碼不需要做任何改變,同樣發(fā)布者需要改變,訂閱者也不會影響。只要之前約定的事件名沒有變化。
3.DOM事件

實(shí)際上,只要我們曾經(jīng)在DOM節(jié)點(diǎn)上綁定過事件函數(shù),那我們就曾經(jīng)使用過發(fā)布-訂閱模式。

document.body.addEventListener('click', function(){
    alert(1);
}, false);
document.body.click(); // 模擬用戶點(diǎn)擊

在這里我們需要監(jiān)控用戶點(diǎn)擊body的動作,但我們沒法預(yù)知用戶點(diǎn)擊的時(shí)間。所以我們訂閱了document.body上的click事件,當(dāng)body節(jié)點(diǎn)被點(diǎn)擊時(shí),body節(jié)點(diǎn)便會像訂閱者發(fā)布這個(gè)消息。

4.自定義事件

如何一步步實(shí)現(xiàn)發(fā)布-訂閱模式

  • 首先制定好誰是發(fā)布者
  • 然后給發(fā)布者添加一個(gè)緩存列表,用于存放回調(diào)函數(shù)以便通知訂閱者。
  • 最后發(fā)布消息的時(shí)候,發(fā)布者會遍歷這個(gè)緩存列表,依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)。
    另外,可以往回調(diào)函數(shù)里填入一些參數(shù),訂閱者可以接受這些參數(shù)。這是很有必要的,比如售樓處可以在發(fā)給訂閱者消息里面添加房子面積,單價(jià)等信息,訂閱者可以接受到消息做各自的處理
 var salesOffices = {}  // 定義售樓處

    salesOffices.clientList = []  // 緩存列表,存放訂閱者的回調(diào)函數(shù)

    salesOffices.listen = function (fn) {  // 增加訂閱者
        this.clientList.push(fn) // 訂閱的消息添加進(jìn)緩存列表
    }

    salesOffices.trigger = function () {  // 發(fā)布消息
        for (var i = 0; i < this.clientList.length; i++) {
            var fn = this.clientList[i];
            fn.apply(this, arguments) // arguments 是發(fā)布消息時(shí)帶上的參數(shù)
        }
    }

    // 接下來進(jìn)行簡單的測試

    salesOffices.listen(function (price, squareMeter) { // 小明
        console.log('價(jià)格' + price)
        console.log('面積' + squareMeter)
    })

    salesOffices.listen(function (price, squareMeter) { // 小紅
        console.log('價(jià)格' + price)
        console.log('面積' + squareMeter)
    })

    salesOffices.trigger(200000, 88)
    salesOffices.trigger(300000, 99)

至此,我們實(shí)現(xiàn)了一個(gè)最簡單的發(fā)布-訂閱模式,但是這里有一些問題,我們看到訂閱者接收到了發(fā)布者的每個(gè)消息,雖然小明只想買88平方米的房子,但是同時(shí)也接受到了99平方米的信息,所以我們需要標(biāo)識一個(gè)key,讓訂閱者制定粵自己感興趣的消息,改寫后代碼如下:

var salesOffices = {}  // 定義售樓處

    salesOffices.clientList = {}  // 緩存列表,存放訂閱者的回調(diào)函數(shù)

    salesOffices.listen = function (key, fn) {  // 增加訂閱者
        if (!this.clientList[key]){  // 如果還沒有訂閱過此類的消息,給該類消息創(chuàng)建一個(gè)緩存列表
            this.clientList[key] = []
        }
        this.clientList[key].push(fn) // 訂閱的消息添加到緩存列表
    }

    salesOffices.trigger = function () {  // 發(fā)布消息

        var key = Array.prototype.shift.call(arguments) // 取出消息類型,也就是key
        var fns = this.clientList[key] // 取出該類型所有的回調(diào)函數(shù)集合

        if (!fns || fns.length === 0) return false; // 沒有訂閱過該消息就返回

        for (var i = 0; i < fns.length; i++) {
            var fn = fns[i]
            fn.apply(this, arguments)
        }
    }


    salesOffices.listen('m88', function (price) {
        console.log(price);
    })

    salesOffices.listen('m99', function (price) {
        console.log(price);
    })

    salesOffices.trigger('m88', 200000)
    salesOffices.trigger('m99', 300000)

很明顯,訂閱者可以只訂閱自己感興趣的事件了。

5.發(fā)布-訂閱模式的通用實(shí)現(xiàn)

小明又想去其他售樓處買房,那么就需要重新寫一個(gè)salesOffices,所以需要做如下改進(jìn),把發(fā)布訂閱的功能提取出來,單獨(dú)放在一個(gè)對象內(nèi):


    var event = {
        clientList: {},
        listen: function (key, fn) {
            if (!this.clientList[key]){
                this.clientList[key] = []
            }
            this.clientList[key].push(fn)
        },
        trigger: function () {
            var key = Array.prototype.shift.call(arguments) // 取出消息類型,也就是key
            var fns = this.clientList[key] // 取出該類型所有的回調(diào)函數(shù)集合

            if (!fns || fns.length === 0) return false; // 沒有訂閱過該消息就返回

            for (var i = 0; i < fns.length; i++) {
                var fn = fns[i]
                fn.apply(this, arguments)
            }
        }
    }

    // 定義一個(gè)installEvent函數(shù),這個(gè)函數(shù)可以給所有對象都動態(tài)安裝發(fā)布-訂閱功能:
    var installEvent = function (obj) {
        for (var i in event) {
            obj[i] = event[i]
        }
    }

    var salesOffices = {}

    installEvent(salesOffices)

    salesOffices.listen('ss88', function (price) {
        console.log(price);
    })

    salesOffices.listen('ss99', function (price) {
        console.log(price)
    })

    salesOffices.listen('ss88', 200000)
    salesOffices.listen('ss99', 300000)
6. 取消訂閱的事件

有時(shí)候我們有這種需求,小明突然不想買房了,為了避免售樓處推送來的信息,可以取消之前訂閱的事件

 event.remove = function (key, fn) {
        var fns = this.clientList[key]

        if (!fns) return false; // 如果key對應(yīng)得消息沒有被人訂閱,則返回
        if (!fn){
            fns && (fns.length = 0) // 如果沒有傳入具體的回調(diào)函數(shù),表示需要取消key對應(yīng)消息的所有訂閱
        }else {
            for (var i = fns.length - 1; i > 0; i--) { // 反向遍歷訂閱的回調(diào)函數(shù)列表
                var _fn = fns[i]
                if (_fn === fn){
                    fns.splice(i, 1) // 刪除對應(yīng)得回調(diào)函數(shù)
                }
            }
        }

    }

    var salesOffices = {}

    installEvent(salesOffices)

    salesOffices.listen('ss88', fn1 = function (price) {
        console.log(price);
    })

    salesOffices.listen('ss99', fn2 = function (price) {
        console.log(price)
    })

    salesOffices.remove('ss88', fn1)
    salesOffices.trigger('ss88', 200000)
7.真實(shí)的例子--網(wǎng)站登錄

加入我們正在開發(fā)一個(gè)網(wǎng)站商場,網(wǎng)站里面有header,nav,購物車等各模塊。這幾個(gè)模塊的渲染有一個(gè)共同的前提條件,就是必須先用ajax異步請求獲取用戶的信息。
至于ajax請求什么時(shí)候能成功返回用戶信息,這點(diǎn)我們沒法確定。更重要的一點(diǎn)是,我們不知道除了購物車,nav,header之外,還有哪些地方需要用到用戶信息。如果他們和用戶之間信息產(chǎn)生了強(qiáng)耦合,比如下面這樣的形式:

login.succ(function(data){
    header.setAcatar(data.avatar) // 設(shè)置header模塊的頭像
    nav.setAvatar(data.avatar) // 設(shè)置導(dǎo)航模塊的頭像
    message.refresh() // 消息列表刷新
    cart.refresh() // 購物車刷新
})

現(xiàn)在登錄模塊是我們負(fù)責(zé)編寫的,我們必須還了解header模塊設(shè)置頭像的方法叫setAvatar,購物車刷新的方法叫refresh.等等各種方法,如果后面需要加上收貨地址的模塊

login.succ(function(data){
    header.setAcatar(data.avatar) // 設(shè)置header模塊的頭像
    nav.setAvatar(data.avatar) // 設(shè)置導(dǎo)航模塊的頭像
    message.refresh() // 消息列表刷新
    cart.refresh() // 購物車刷新
    adress.refresh()  // 增加
})

這時(shí)候,又要重構(gòu)代碼。
當(dāng)我們用發(fā)布訂閱模式重寫后,對用戶感興趣的業(yè)務(wù)模塊將自定訂閱登錄成功的消息事件。登錄成功后,登錄模塊只需要發(fā)布登錄信息,而業(yè)務(wù)方接受消息之后,可以自己做各自的業(yè)務(wù)處理。

$.ajax('http://xxx.xx.com/login',function(data){
    login.trigger('loginsuccess', data)
})

各模塊監(jiān)聽登錄成功的消息

var header = (function () {
        login.listen('loginsuccess', function (data) {
            header.setAvator(data.avator)
        });
        return {
            setAvator: function(data) {
                console.log('設(shè)置header的頭像')
            }
        }
    })()

這時(shí)候,如果添加一個(gè)刷新收貨地址的行為,就可以讓收貨地址模塊開發(fā)人員去訂閱登錄成功這個(gè)事件

    var adderss = (function () {
        login.listen('loginsuccess', function (data) {
            adderss.refresh(data)
        });
        return {
            refresh: function (data) {
                console.log('刷新地址')
            }
        }
    })()
8. 全局的發(fā)布-訂閱對象

用一個(gè)全局的Event對象來實(shí)現(xiàn),訂閱者不需要了解消息來自哪個(gè)發(fā)布者,發(fā)布者也不需要消息會推送給訂閱者,Event作為一個(gè)中介者的角色來吧發(fā)布者和訂閱者聯(lián)系到一起,代碼如下:

var Event = (function () {

        var clientList = {},
            listen,
            trigger,
            remove;

        listen = function (key, fn) {
            if (!clientList[key]) {
                clientList[key] = [];
            }
            clientList[key].push(fn);
        }

        trigger = function() {
            var key = Array.prototype.shift.call(arguments) // 取出消息類型,也就是key
            var fns = clientList[key] // 取出該類型所有的回調(diào)函數(shù)集合

            if (!fns || fns.length === 0) return false; // 沒有訂閱過該消息就返回

            for (var i = 0; i < fns.length; i++) {
                var fn = fns[i]
                fn.apply(this, arguments)
            }
        }

        remove = function (key, fn) {
            var fns = clientList[key]

            if (!fns) return false; // 如果key對應(yīng)得消息沒有被人訂閱,則返回
            if (!fn){
                fns && (fns.length = 0) // 如果沒有傳入具體的回調(diào)函數(shù),表示需要取消key對應(yīng)消息的所有訂閱
            }else {
                for (var i = fns.length - 1; i > 0; i--) { // 反向遍歷訂閱的回調(diào)函數(shù)列表
                    var _fn = fns[i]
                    if (_fn === fn){
                        fns.splice(i, 1) // 刪除對應(yīng)得回調(diào)函數(shù)
                    }
                }
            }
        }

        return {
            listen: listen,
            trigger: trigger,
            remove: remove
        }

    })()


    Event.listen('ss88', function (data) {
        console.log(data);
    })

    Event.trigger('ss88', 222)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容