JavaScript設計模式之發布訂閱模式(Publish/Subscribe)

發布/訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關系,當一個對象的狀態發生改變時,所有依賴于它的對象都將得到通知。在 JavaScript 開發中,我們一般用事件模型來替代傳統的發布/訂閱模式。

遍地的發布訂閱現象

如今的信息化時代,發布/訂閱模式的應用可以說非常廣泛,比如微信公眾號就是典型的發布/訂閱模式,公眾號發布一條信息,所有的訂閱者都會收到。

有人可能也會想到經常收到的各種廣告短信信息(有的可能是被動訂閱),其實發送短信通知或廣告也是一個典型的發布/訂閱模式。

發布/訂閱模式可以廣泛用于異步編程中,代替傳遞回掉函數的方案,比如,我們可以訂閱 ajax 請求的 error、succ 等事件。

另外發表訂閱讓兩個對象松耦合在一起,不必了解彼此細節,當有新的訂閱者出現時,發布者的代碼不需要任何修改。同樣發布者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們。

定義

發布訂閱模式,它定義了一種一對多的關系,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知所有的觀察者對象,使得它們能夠自動更新自己。

使用發布訂閱模式的好處:

  • 支持簡單的廣播通信,自動通知所有已經訂閱過的對象。
  • 頁面載入后目標對象很容易與觀察者存在一種動態關聯,增加了靈活性。
  • 目標對象與觀察者之間的抽象耦合關系能夠單獨擴展以及重用。

使用實例

自定義發表訂閱

我們來嘗試一個自定義的發布訂閱模式,那么如何實現發布訂閱呢

  • 指定一個發布者
  • 給發布者添加一個緩沖列表,用于存放回調函數以用于通知訂閱者
  • 發布消息時,發布者遍歷緩存列表,依次觸發每個訂閱者的回調函數

一個簡單的天氣狀態訂閱

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

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

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

以上,已經實現了一個最簡單的發布—訂閱模式,還可以為訂閱者增加自選功能,訂閱自己想要的消息,也可以增加取消訂閱的事件。

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('風力:'+ wind);
});

day.publish('天氣', "8級風");

實戰之網站登錄

網站登錄是最常見的形式,通常在登錄以后我們會ajax異步請求獲取用戶信息,比如顯示用戶名字、頭像等信息在header模塊,而這兩個字段都是來自用戶登錄后返回的信息。至于 ajax 請求什么時候能成功返回用戶信息,這點我們沒有辦法確定,雖然現在看起來和發布訂閱模式沒關系,因為異步的問題通常也可以回調函數來解決。

我們不知道除了 header 頭部、nav 導航、消息列表、購物車之外,將來還有哪些模塊需要使用這些用戶信息。如果它們和用戶信息模塊產生了強耦合,比如下面這樣的形式:

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

現在登錄模塊是我們負責編寫的,但我們還必須了解 header 模塊里設置頭像的方法叫 setAvatar、購物車模塊里刷新的方法叫 refresh,這種耦合性會使程序變得僵硬,header 模塊不能隨意再改變 setAvatar 的方法名,它自身的名字也不能被改為 header1、header2。 這是針對具 體實現編程的典型例子,針對具體實現編程是不被贊同的。

某一個,項目新增加收獲地址管理模塊:

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

現在我們用發布訂閱重寫,對用戶信息感興趣的業務模塊將自行訂閱登錄成功的消息事件。 當登錄成功時,登錄模塊只需要發布登錄成功的消息,而業務方接受到消息之后,就會開始進行各自的業務處理,登錄模塊并不關心業務方究竟要做什么,也不想去了解它們的內部細節。改善后的代碼如下:

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

各模塊監聽登錄成功的消息:

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

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

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

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

總結

發布訂閱的使用場合就是:當一個對象的改變需要同時改變其它對象,并且它不知道具體有多少對象需要改變的時候,就應該考慮使用觀察者模式。

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

另外, 發布—訂閱模式雖然可以弱化對象之間的聯系,但如果過度使用的話,對象和對象之間的必要聯系也將被深埋在背后,會導致程序難以跟蹤維護和理解。特別是有多個發布者和訂閱者嵌套到一起的時候,要跟蹤一個 bug 不是件輕松的事情。


參考引用資料

《JavaScript設計模式與開發實踐》

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 工廠模式類似于現實生活中的工廠可以產生大量相似的商品,去做同樣的事情,實現同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,807評論 2 17
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,817評論 25 708
  • 接觸前端兩三個月的時候,那時候只是聽說設計模式很重要,然后我就去讀了一本設計模式的書,讀了一部分,也不知道這些設計...
    艱苦奮斗的侯小憨閱讀 3,085評論 2 39
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 慌不擇亂的從錢包里抽出一張20的紙幣攥在手里,實在忍不了被蚊子叮的那個包,眼看著天黑下來也要騎上自行車沖出家門去買...
    Hahaha1223閱讀 277評論 0 0