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

發(fā)布/訂閱模式又叫觀察者模式,它定義對(duì)象間的一種一對(duì)多的依賴(lài)關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴(lài)于它的對(duì)象都將得到通知。在 JavaScript 開(kāi)發(fā)中,我們一般用事件模型來(lái)替代傳統(tǒng)的發(fā)布/訂閱模式。

遍地的發(fā)布訂閱現(xiàn)象

如今的信息化時(shí)代,發(fā)布/訂閱模式的應(yīng)用可以說(shuō)非常廣泛,比如微信公眾號(hào)就是典型的發(fā)布/訂閱模式,公眾號(hào)發(fā)布一條信息,所有的訂閱者都會(huì)收到。

有人可能也會(huì)想到經(jīng)常收到的各種廣告短信信息(有的可能是被動(dòng)訂閱),其實(shí)發(fā)送短信通知或廣告也是一個(gè)典型的發(fā)布/訂閱模式。

發(fā)布/訂閱模式可以廣泛用于異步編程中,代替?zhèn)鬟f回掉函數(shù)的方案,比如,我們可以訂閱 ajax 請(qǐng)求的 error、succ 等事件。

另外發(fā)表訂閱讓兩個(gè)對(duì)象松耦合在一起,不必了解彼此細(xì)節(jié),當(dāng)有新的訂閱者出現(xiàn)時(shí),發(fā)布者的代碼不需要任何修改。同樣發(fā)布者需要改變時(shí),也不會(huì)影響到之前的訂閱者。只要之前約定的事件名沒(méi)有變化,就可以自由地改變它們。

定義

發(fā)布訂閱模式,它定義了一種一對(duì)多的關(guān)系,讓多個(gè)觀察者對(duì)象同時(shí)監(jiān)聽(tīng)某一個(gè)主題對(duì)象,這個(gè)主題對(duì)象的狀態(tài)發(fā)生變化時(shí)就會(huì)通知所有的觀察者對(duì)象,使得它們能夠自動(dòng)更新自己。

使用發(fā)布訂閱模式的好處:

  • 支持簡(jiǎn)單的廣播通信,自動(dòng)通知所有已經(jīng)訂閱過(guò)的對(duì)象。
  • 頁(yè)面載入后目標(biāo)對(duì)象很容易與觀察者存在一種動(dòng)態(tài)關(guān)聯(lián),增加了靈活性。
  • 目標(biāo)對(duì)象與觀察者之間的抽象耦合關(guān)系能夠單獨(dú)擴(kuò)展以及重用。

使用實(shí)例

自定義發(fā)表訂閱

我們來(lái)嘗試一個(gè)自定義的發(fā)布訂閱模式,那么如何實(shí)現(xiàn)發(fā)布訂閱呢

  • 指定一個(gè)發(fā)布者
  • 給發(fā)布者添加一個(gè)緩沖列表,用于存放回調(diào)函數(shù)以用于通知訂閱者
  • 發(fā)布消息時(shí),發(fā)布者遍歷緩存列表,依次觸發(fā)每個(gè)訂閱者的回調(diào)函數(shù)

一個(gè)簡(jiǎn)單的天氣狀態(tài)訂閱

var Weather = {
    list: [], // 緩存列表
    listen: function(fn) { // 增加訂閱者  
        this.list.push(fn)
    },
    publish: function() { // 發(fā)布消息
        for(var i=0,fn; fn=this.list[i++];) {
            fn.apply(this,arguments);
        }
    }
};

// 訂閱消息
Weather.listen(function(weather, wind){
    console.log('天氣:' + weather, '風(fēng)力:'+ wind);
})

// 發(fā)布消息
Weather.publish("晴天","微風(fēng)"); // 天氣:晴天 風(fēng)力:微風(fēng)
Weather.publish("雷陣雨","5級(jí)風(fēng)"); // 天氣:雷陣雨 風(fēng)力:5級(jí)風(fēng)

以上,已經(jīng)實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的發(fā)布—訂閱模式,還可以為訂閱者增加自選功能,訂閱自己想要的消息,也可以增加取消訂閱的事件。

var PubSub = {
    list: [],
    listen: function(key, fn){
        if(!this.list[key]) {
            this.list[key]=[];
        }
        this.list[key].push(fn);
    },
    publish: function(){
        var key = Array.prototype.shift.call(arguments),
            fns = this.list[key];
        if(!fns || fns.length === 0) {
            return false;
        }
        for(var i = 0, fn; fn = fns[i++];){
            fn.apply(this, arguments);
        }
    }
}

//
var installEvent = function(obj) {
    for (var i in PubSub) {
        obj[i] = PubSub[i];
    }
};

var day = {}
installEvent(day);

day.listen('天氣', function(wind) {
    console.log('風(fēng)力:'+ wind);
});

day.publish('天氣', "8級(jí)風(fēng)");

實(shí)戰(zhàn)之網(wǎng)站登錄

網(wǎng)站登錄是最常見(jiàn)的形式,通常在登錄以后我們會(huì)ajax異步請(qǐng)求獲取用戶(hù)信息,比如顯示用戶(hù)名字、頭像等信息在header模塊,而這兩個(gè)字段都是來(lái)自用戶(hù)登錄后返回的信息。至于 ajax 請(qǐng)求什么時(shí)候能成功返回用戶(hù)信息,這點(diǎn)我們沒(méi)有辦法確定,雖然現(xiàn)在看起來(lái)和發(fā)布訂閱模式?jīng)]關(guān)系,因?yàn)楫惒降膯?wèn)題通常也可以回調(diào)函數(shù)來(lái)解決。

我們不知道除了 header 頭部、nav 導(dǎo)航、消息列表、購(gòu)物車(chē)之外,將來(lái)還有哪些模塊需要使用這些用戶(hù)信息。如果它們和用戶(hù)信息模塊產(chǎn)生了強(qiáng)耦合,比如下面這樣的形式:

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

現(xiàn)在登錄模塊是我們負(fù)責(zé)編寫(xiě)的,但我們還必須了解 header 模塊里設(shè)置頭像的方法叫 setAvatar、購(gòu)物車(chē)模塊里刷新的方法叫 refresh,這種耦合性會(huì)使程序變得僵硬,header 模塊不能隨意再改變 setAvatar 的方法名,它自身的名字也不能被改為 header1、header2。 這是針對(duì)具 體實(shí)現(xiàn)編程的典型例子,針對(duì)具體實(shí)現(xiàn)編程是不被贊同的。

某一個(gè),項(xiàng)目新增加收獲地址管理模塊:

login.succ(function(data){ 
    header.setAvatar( data.avatar);
    nav.setAvatar( data.avatar ); 
    message.refresh(); 
    address.refresh(); // 新增加收獲地址
});

現(xiàn)在我們用發(fā)布訂閱重寫(xiě),對(duì)用戶(hù)信息感興趣的業(yè)務(wù)模塊將自行訂閱登錄成功的消息事件。 當(dāng)?shù)卿洺晒r(shí),登錄模塊只需要發(fā)布登錄成功的消息,而業(yè)務(wù)方接受到消息之后,就會(huì)開(kāi)始進(jìn)行各自的業(yè)務(wù)處理,登錄模塊并不關(guān)心業(yè)務(wù)方究竟要做什么,也不想去了解它們的內(nèi)部細(xì)節(jié)。改善后的代碼如下:

$.ajax( 'http://xxx.com?login', function(data){ // 登錄成功 
    login.trigger( 'loginSucc', data); // 發(fā)布登錄成功的消息
});

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

var header = (function() { // header 模塊
    login.listen( 'loginSucc', function(data) {
        header.setAvatar( data.avatar );
    });
    return {
        setAvatar: function(data) {
            console.log( '設(shè)置 header 模塊的頭像');
        }
    }
})();

var nav = (function() {  // nav 模塊
    login.listen('loginSucc', function(data) {
        nav.setAvatar( data.avatar );
    });
    return {
        setAvatar: function(avatar) {
            console.log( '設(shè)置 nav 模塊的頭像');
        }
    }
})();

如果有一天在登錄完成之 后,又增加一個(gè)刷新收貨地址列表的行為,那么只要在收貨地址模塊里加上監(jiān)聽(tīng)消息的方法即可,而這可以讓開(kāi)發(fā)該模塊的同事自己完成,你作為登錄模塊的開(kāi)發(fā)者,永遠(yuǎn)不用再關(guān)心這些行為了

var address = (function(){ // 收獲地址模塊 
    login.listen('loginSucc', function(obj){
        address.refresh(obj);
    });
    return {
        refresh: function( avatar ){
            console.log( '刷新收貨地址列表' ); 
        }
    } 
})();

總結(jié)

發(fā)布訂閱的使用場(chǎng)合就是:當(dāng)一個(gè)對(duì)象的改變需要同時(shí)改變其它對(duì)象,并且它不知道具體有多少對(duì)象需要改變的時(shí)候,就應(yīng)該考慮使用觀察者模式。

總的來(lái)說(shuō),發(fā)布訂閱模式所做的工作就是在解耦,讓耦合的雙方都依賴(lài)于抽象,而不是依賴(lài)于具體。從而使得各自的變化都不會(huì)影響到另一邊的變化。

另外, 發(fā)布—訂閱模式雖然可以弱化對(duì)象之間的聯(lián)系,但如果過(guò)度使用的話,對(duì)象和對(duì)象之間的必要聯(lián)系也將被深埋在背后,會(huì)導(dǎo)致程序難以跟蹤維護(hù)和理解。特別是有多個(gè)發(fā)布者和訂閱者嵌套到一起的時(shí)候,要跟蹤一個(gè) bug 不是件輕松的事情。


參考引用資料

《JavaScript設(shè)計(jì)模式與開(kāi)發(fā)實(shí)踐》

湯姆大叔的博客——深入理解JavaScript系列

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

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

  • 工廠模式類(lèi)似于現(xiàn)實(shí)生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實(shí)現(xiàn)同樣的效果;這時(shí)候需要使用工廠模式。簡(jiǎn)單...
    舟漁行舟閱讀 8,130評(píng)論 2 17
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,037評(píng)論 25 709
  • 接觸前端兩三個(gè)月的時(shí)候,那時(shí)候只是聽(tīng)說(shuō)設(shè)計(jì)模式很重要,然后我就去讀了一本設(shè)計(jì)模式的書(shū),讀了一部分,也不知道這些設(shè)計(jì)...
    艱苦奮斗的侯小憨閱讀 3,191評(píng)論 2 39
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,554評(píng)論 19 139
  • 慌不擇亂的從錢(qián)包里抽出一張20的紙幣攥在手里,實(shí)在忍不了被蚊子叮的那個(gè)包,眼看著天黑下來(lái)也要騎上自行車(chē)沖出家門(mén)去買(mǎi)...
    Hahaha1223閱讀 303評(píng)論 0 0

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