JavaScript中發佈/訂閱模式(觀察者模式)

發佈/訂閱模式的前身-觀察者模式

觀察者模式定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並自動更新。觀察者模式屬於行爲型模式,行爲型模式關注的是對象之間的通訊,觀察者模式就是觀察者和被觀察者之間的通訊。

觀察者模式有一個別名叫“發佈-訂閱模式”,或者說是“訂閱-發佈模式”,訂閱者和訂閱目標是聯繫在一起的,當訂閱目標發生改變時,逐個通知訂閱者。

什麼是發佈/訂閱模式

其實24種基本的設計模式中並沒有發佈訂閱模式,上面也說了,他只是觀察者模式的一個別稱。
但是經過時間的沉澱,似乎他已經強大了起來,已經獨立於觀察者模式,成爲另外一種不同的設計模式。
在現在的發佈訂閱模式中,稱爲發佈者的消息發送者不會將消息直接發送給訂閱者,這意味着發佈者和訂閱者不知道彼此的存在。在發佈者和訂閱者之間存在第三個組件,稱爲消息代理或調度中心或中間件,它維持着發佈者和訂閱者之間的聯繫,過濾所有發佈者傳入的消息並相應地分發它們給訂閱者。

根本作用

  • 廣泛應用於異步編程中(替代了傳遞迴調函數)
  • 對象之間鬆散耦合的編寫代碼

基本案例介紹

背景:成都老媽兔頭真香,買的人太多需要預定才能買到,所以顧客就等於了訂閱者,訂閱老媽兔頭。
而老媽兔頭有貨了得通知顧客來買啊,不然沒有錢賺,得通知所有的訂閱者有貨了來提兔頭,這時老媽兔頭這家店就是發佈者。

/*兔頭店*/        
var shop={
    listenList:[],//緩存列表
    addlisten:function(fn){//增加訂閱者
        this.listenList.push(fn);
    },
    trigger:function(){//發佈消息
        for(var i=0,fn;fn=this.listenList[i++];){
            fn.apply(this,arguments);
        }
    }
}

/*小明訂閱了商店*/
shop.addlisten(function(taste){
    console.log("通知小明,"+taste+"味道的好了");
});
/*小龍訂閱了商店*/
shop.addlisten(function(taste){
    console.log("通知小龍,"+taste+"味道的好了");
});
/*小紅訂閱了商店*/
shop.addlisten(function(taste){
    console.log("通知小紅,"+taste+"味道的好了");
});    

// 發佈訂閱
shop.trigger("中辣");

//console
通知小明,中辣味道的好了
通知小龍,中辣味道的好了
通知小紅,中辣味道的好了

案例升級

上面的案例存在問題,因爲在觸發的時候是將所以的訂閱都觸發了,並沒有區分和判斷,所以需要一個Key來區分訂閱的類型,並且根據不同的情況觸發。而且訂閱是可以取消的。

升級思路:

  • 創建一個對象(緩存列表)
  • addlisten方法用來把訂閱回調函數fn都加到緩存列表listenList中
  • trigger方法取到arguments裏第一個當做key,根據key值去執行對應緩存列表中的函數
  • remove方法可以根據key值取消訂閱
/*兔頭店*/        
var shop={
    listenList:{},//緩存對象
    addlisten:function(key,fn){

        // 沒有沒有key給個初值避免調用報錯
        if (!this.listenList[key]) {
            this.listenList[key] = [];
        }
        // 增加訂閱者,一個key就是一種訂閱類型
        this.listenList[key].push(fn);
    },
    trigger:function(){
        const key = Array.from(arguments).shift()
        const fns = this.listenList[key]
        
        // 這裏排除兩種特殊情況,第一種爲觸發的一種從未訂閱的類型,第二種訂閱後取消了所有訂閱的
        if(!fns || fns.length===0){
            return false;
        }

        // 發佈消息,觸發同類型的所有訂閱,
        fns.forEach((fn)=>{
            fn.apply(this,arguments);
        })

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

    },

    remove:function(key,fn){
        var fns=this.listenList[key];//取出該類型的對應的消息集合
        if(!fns){//如果對應的key沒有訂閱直接返回
            return false;
        }
        if(!fn){//如果沒有傳入具體的回掉,則表示需要取消所有訂閱
            fns && (fns.length=0);
        }else{
            for(var l=fns.length-1;l>=0;l--){//遍歷回掉函數列表
                if(fn===fns[l]){ // 這裏是傳入地址的比較,所以不能直接用匿名函數了
                    fns.splice(l,1);//刪除訂閱者的回掉
                }
            }
        }
    }

}

function xiaoming(taste){
    console.log("通知小明,"+taste+"味道的好了");
}

function xiaolong(taste){
    console.log("通知小龍,"+taste+"味道的好了");
}

function xiaohong(taste){
    console.log("通知小紅,"+taste+"味道的好了");
}

// 小明訂閱了商店
shop.addlisten('中辣',xiaoming);

shop.addlisten('特辣',xiaoming);

// 小龍訂閱了商店
shop.addlisten('微辣',xiaolong);

// 小紅訂閱了商店
shop.addlisten('中辣',xiaohong);

// 小紅突然不想吃了
shop.remove("中辣",xiaohong);

// 中辣口味做好後,發佈訂閱
shop.trigger("中辣");

shop.trigger("微辣");

shop.trigger("特辣");

發佈-訂閱的順序探討

我們通常所看到的都是先訂閱再發布,但是必須要遵守這種順序嗎?答案是不一定的。如果發佈者先發布一條消息,但是此時還沒有訂閱者訂閱此消息,我們可以不讓此消息消失於宇宙之中。就如同QQ離線消息一樣,離線的消息被保存在服務器中,接收人下次登錄之後,纔會收到此消息。同樣的,我們可以建立一個存放離線事件的堆棧,當事件發佈的時候,如果此時還沒有訂閱者訂閱這個事件,我們暫時把發佈事件的動作包裹在一個函數裏,這些包裝函數會被存入堆棧中,等到有對象來訂閱事件的時候,我們將遍歷堆棧並依次執行這些包裝函數,即重發裏面的事件,不過離線事件的生命週期只有一次,就像qq未讀消息只會提示你一次一樣。

小結

發佈-訂閱的優勢很明顯,做到了時間上的解耦和對象之間的解耦,從架構上看,MVC,MVVM都少不了發佈-訂閱的參與。
同樣的node中的EventEmitter也是發佈訂閱的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章