釋出訂閱模式及多種實現方式:
1、何為觀察者模式?
觀察者模式,又可以稱之為釋出-訂閱模式,觀察者,顧名思義,就是一個監聽者,類似監聽器的存在,一旦被觀察/監聽的目標發生的情況,就會被監聽者發現,這麼想來目標發生情況到觀察者知道情況,其實是由目標將情況傳送到觀察者的。
觀察者模式多用於實現訂閱功能的場景,例如微博的訂閱,當我們訂閱了某個人的微博賬號,當這個人釋出了新的訊息,就會通知我們。
2、觀察者模式解決的問題?
解決主體物件與觀察者之間功能耦合。
3、實現觀察者步驟:
(1)建立一個觀察者:
把觀察者或者訊息系統看作一個物件,那麼他應該包含兩個方法,一個是接受訊息,一個是向中轉站傳送相應訊息。
首先,我們把需要的把觀察者物件創建出來,有一個訊息容器和三個方法,分別是,訂閱訊息的方法,取消訂閱訊息的方法,傳送訂閱訊息的方法。
var Observer = (function(){ //防止訊息佇列暴露而被篡改,所以將訊息容器作為靜態私有變數儲存 var __messsages = {}; return { //註冊資訊介面 regist: function(){}, //釋出資訊的介面 fire: function(){}, //移除資訊介面 remove: function(){} } })();
觀察者物件的雛形出來了,我們需要做的事情就是實現這三個方法
(2)實現訂閱方法:
我們首先實現訊息註冊方法,註冊方法的作用是將訂閱者註冊的訊息推入到訊息佇列中,因此我們需要接受兩個引數:訊息型別和以及相應的處理動作,在推入到訊息佇列時如果此訊息不存在應該建立一個該訊息型別並將該訊息放入到訊息佇列中,如果此訊息存在則應該將訊息執行方法推入該訊息對應的執行方法佇列中,這麼做目的是保證多個模組註冊同一個訊息能順利執行。
regist: function(type,fn){ //如果此訊息不存在,則應該建立一個該訊息型別 if(typeof __messages[type] === 'undefined'){ //將物件推入到該訊息對應的動作執行佇列中 __messages[type] = [fn]; //如果此訊息存在 }else{ //將動作方法推入該訊息對應的動作執行序列中 __messages[type].push(fn); } }
(3)實現釋出方法:
對於釋出訊息方法,其功能是當觀察者釋出一個訊息時將所有訂閱者訂閱的訊息一次執行。
故應該接受兩個引數,訊息型別以及動作執行時需要傳遞的引數,當然在這裡訊息型別是必須的。在執行訊息佇列之前校驗訊息的存在是很有必要的。
然後遍歷訊息執行方法佇列,並依此執行。
然後將訊息類別以及傳遞的引數打包後依次傳入訊息佇列執行方法中。
fire: function(type,args){ //如果該訊息沒有被註冊,則返回 if(!__messages[type]){ return ; //定義訊息資訊 var events = { type:type, args:args||{} }, i=0; len = __messages[type].length; for(;i<len;i++){ //依次執行註冊的訊息對應的動作序列 __messages[type][i].call(this,events); } }
(4)實現登出方法:
最後是訊息登出方法,其功能是將訂閱者登出的訊息從訊息佇列清除,因此我們也需要兩個引數,即訊息型別以及執行的某一個動作。當然為了避免刪除訊息動作時訊息不存在情況的出現,對訊息佇列中訊息的存在性校驗也很有必要的。
remove:function(type,fn){ //如果訊息動作佇列存在 if(__messages[type] instanceof Array){ //從最後一個訊息動作遍歷 var i = __messages[type].length-1; for(;i>=0;i--){ //如果存在該動作則在訊息動作中移除相應動作 __messages[type][i] === fn && __messages[type].splice(i,1); } } }
3、其他實現觀察者方式(使用ES6 Class 方式實現鏈式呼叫的觀察者模式):
/** * 釋出訂閱模式(觀察者模式) * handles: 事件處理函式集合 * on: 訂閱事件 * emit: 釋出事件 * off: 刪除事件 **/ class PubSub { constructor() { this.handles = {}; } // 訂閱事件 on(eventType, handle) { if (!this.handles.hasOwnProperty(eventType)) { this.handles[eventType] = []; } if (typeof handle == 'function') { this.handles[eventType].push(handle); } else { throw new Error('缺少回撥函式'); } return this; } // 釋出事件 emit(eventType, ...args) { if (this.handles.hasOwnProperty(eventType)) { this.handles[eventType].forEach((item, key, arr) => { item.apply(null, args); }) } else { throw new Error(`"${eventType}"事件未註冊`); } return this; } // 刪除事件 off(eventType, handle) { if (!this.handles.hasOwnProperty(eventType)) { throw new Error(`"${eventType}"事件未註冊`); } else if (typeof handle != 'function') { throw new Error('缺少回撥函式'); } else { this.handles[eventType].forEach((item, key, arr) => { if (item == handle) { arr.splice(key, 1); } }) } return this; // 實現鏈式操作 } } // 下面做一些騷操作 let callback = function () { console.log('you are so nice'); } let pubsub = new PubSub(); pubsub.on('completed', (...args) => { console.log(args.join(' ')); }).on('completed', callback); pubsub.emit('completed', 'what', 'a', 'fucking day'); pubsub.off('completed', callback); pubsub.emit('completed', 'fucking', 'again');