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

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

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

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

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

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

  • 購房者不用天天打電話給售樓處鹅搪,在合適的時間站绪,售樓處會作為發(fā)布者通知給所有的訂閱者
  • 購房者和售樓處之前不再強(qiáng)耦合的在一起,當(dāng)有新的購房者出現(xiàn)時丽柿,只需要留下手機(jī)號碼恢准,售樓處不需要關(guān)系購房者的任何情況,只需要知道誰訂閱了購房消息即可甫题。
    第一條說明發(fā)布訂閱模式可以廣泛引用于異步編程中馁筐,這是一種替代傳遞回調(diào)函數(shù)的方案。比如坠非,訂閱ajax請求的success敏沉,error事件。或者如果想在動畫的每一幀完成之后做一些事情盟迟,那我們可以訂閱一個事件秋泳,然后在動畫的每一幀完成之后去發(fā)布這個事件。在異步變成中使用發(fā)布-訂閱模式攒菠,我們就無序過多關(guān)注對象在異步運(yùn)行中的內(nèi)部狀態(tài)迫皱,而只需要訂閱感興趣的事件發(fā)生點(diǎn)。
    第二條說明發(fā)布-訂閱模式可以取代對象之間硬編碼的通知機(jī)制辖众,一個對象不再顯試的嗲用另外一個對象的某個接口卓起。發(fā)布-訂閱模式讓兩個對象松耦合的聯(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)擊的時間。所以我們訂閱了document.body上的click事件誊爹,當(dāng)body節(jié)點(diǎn)被點(diǎn)擊時蹬刷,body節(jié)點(diǎn)便會像訂閱者發(fā)布這個消息。

4.自定義事件

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

  • 首先制定好誰是發(fā)布者
  • 然后給發(fā)布者添加一個緩存列表频丘,用于存放回調(diào)函數(shù)以便通知訂閱者办成。
  • 最后發(fā)布消息的時候,發(fā)布者會遍歷這個緩存列表搂漠,依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)迂卢。
    另外,可以往回調(diào)函數(shù)里填入一些參數(shù)桐汤,訂閱者可以接受這些參數(shù)而克。這是很有必要的,比如售樓處可以在發(fā)給訂閱者消息里面添加房子面積怔毛,單價等信息员萍,訂閱者可以接受到消息做各自的處理
 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ù)
        }
    }

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

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

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

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

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

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

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

    salesOffices.listen = function (key, fn) {  // 增加訂閱者
        if (!this.clientList[key]){  // 如果還沒有訂閱過此類的消息,給該類消息創(chuàng)建一個緩存列表
            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)

小明又想去其他售樓處買房梢卸,那么就需要重新寫一個salesOffices,所以需要做如下改進(jìn)副女,把發(fā)布訂閱的功能提取出來蛤高,單獨(dú)放在一個對象內(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)
            }
        }
    }

    // 定義一個installEvent函數(shù)碑幅,這個函數(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. 取消訂閱的事件

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

 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ā)一個網(wǎng)站商場裹赴,網(wǎng)站里面有header喜庞,nav,購物車等各模塊棋返。這幾個模塊的渲染有一個共同的前提條件延都,就是必須先用ajax異步請求獲取用戶的信息。
至于ajax請求什么時候能成功返回用戶信息睛竣,這點(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()  // 增加
})

這時候距误,又要重構(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的頭像')
            }
        }
    })()

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

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

用一個全局的Event對象來實(shí)現(xiàn)怔软,訂閱者不需要了解消息來自哪個發(fā)布者,發(fā)布者也不需要消息會推送給訂閱者择镇,Event作為一個中介者的角色來吧發(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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吝梅,隨后出現(xiàn)的幾起案子虱疏,更是在濱河造成了極大的恐慌,老刑警劉巖苏携,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件做瞪,死亡現(xiàn)場離奇詭異,居然都是意外死亡兜叨,警方通過查閱死者的電腦和手機(jī)穿扳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來国旷,“玉大人矛物,你說我怎么就攤上這事」虻” “怎么了他去?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵饱岸,是天一觀的道長饮亏。 經(jīng)常有香客問我慨绳,道長,這世上最難降的妖魔是什么被环? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任糙及,我火速辦了婚禮,結(jié)果婚禮上筛欢,老公的妹妹穿的比我還像新娘浸锨。我一直安慰自己唇聘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布柱搜。 她就那樣靜靜地躺著迟郎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪聪蘸。 梳的紋絲不亂的頭發(fā)上宪肖,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音健爬,去河邊找鬼控乾。 笑死,一個胖子當(dāng)著我的面吹牛浑劳,可吹牛的內(nèi)容都是我干的阱持。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼魔熏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鸽扁?” 一聲冷哼從身側(cè)響起蒜绽,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桶现,沒想到半個月后躲雅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骡和,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年相赁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慰于。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡钮科,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出婆赠,到底是詐尸還是另有隱情绵脯,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布休里,位于F島的核電站蛆挫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏妙黍。R本人自食惡果不足惜悴侵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拭嫁。 院中可真熱鬧可免,春花似錦抓于、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逮刨,卻和暖如春呕缭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背修己。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工恢总, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人睬愤。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓片仿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親尤辱。 傳聞我的和親對象是個殘疾皇子砂豌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容