觀察者模式定義了對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知,並自動更新。觀察者模式屬於行爲型模式,行爲型模式關注的是對象之間的通信,觀察者模式就是觀察者和被觀察者之間的通信。node
觀察者模式有一個別名叫「發佈-訂閱模式」,或者說是「訂閱-發佈模式」,訂閱者和訂閱目標是聯繫在一塊兒的,當訂閱目標發生改變時,逐個通知訂閱者。編程
其實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來區分訂閱的類型,而且根據不一樣的狀況觸發。並且訂閱是能夠取消的。服務器
升級思路:架構
/*兔頭店*/ 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未讀消息只會提示你一次同樣。app
發佈-訂閱的優點很明顯,作到了時間上的解耦和對象之間的解耦,從架構上看,MVC,MVVM都少不了發佈-訂閱的參與。
一樣的node中的EventEmitter也是發佈訂閱的異步