發佈/訂閱模式的前身-觀察者模式
觀察者模式定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知,並自動更新。觀察者模式屬於行爲型模式,行爲型模式關注的是對象之間的通訊,觀察者模式就是觀察者和被觀察者之間的通訊。
觀察者模式有一個別名叫“發佈-訂閱模式”,或者說是“訂閱-發佈模式”,訂閱者和訂閱目標是聯繫在一起的,當訂閱目標發生改變時,逐個通知訂閱者。
什麼是發佈/訂閱模式
其實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也是發佈訂閱的