發佈訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知。在 JavaScript 開發中,咱們通常用事件模型 來替代傳統的發佈—訂閱模式。es6
異步:
發佈訂閱模式能夠普遍應用於異步編程中,這是一種替代傳遞迴調函數的方案。 好比,咱們能夠訂閱 ajax 請求的 error、succ 等事件。 或者若是想在動畫的每一幀完成以後作一些事情,那咱們能夠訂閱一個事件,而後在動畫的每一幀完成以後發佈這個事件。在異步編程中,使用發佈—訂閱模式,咱們就無需過多關注對象在異步運行期間的內部狀態,而只須要訂閱感興趣的事件發生點。ajax
解耦
發佈訂閱模式能夠取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另一個對象的某個接口。發佈—訂閱模式讓兩個對象鬆耦合地聯繫在一塊兒,雖然不太清楚彼此的細節,但這不影響它們之間相互通訊。當有新的訂閱者出現時,發佈者的代碼不須要任何修改;一樣發佈者須要改變時,也不會影響到以前的訂閱者。只要以前約定的事件名沒有變化,就 能夠自由地改變它們。編程
觀察者(或者發佈者)本質上是一個對象,它有一個屬性,做爲存放事件的容器,和三個方法,分別是訂閱消息,取消訂閱消息和發佈消息。設計模式
var observer = { events: {}, // 按消息類型存放事件,屬性名是消息類型,屬性值都是數組 listen: function(){}, //訂閱,按消息類型將事件推入到事件容器中 emit: function(){}, // 發佈,按消息類型將訂閱者訂閱的消息一次性執行完 remove: function(){}, // 取消訂閱,按消息類型將訂閱者訂閱的事件從事件容器中移除 }
var observer = { events: {}, listen: function(type, cb){ if(!this.events[type]){ this.events[type] = []; } this.events[type].push(cb); }, emit: function(type){ if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){ for(let i = 0, task; task = this.messages[type][i++];){ task(); } } }, remove: function(type, fn){ if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){ for(let i = this.messages[type].length - 1;i>=0;i--){ if(this.messages[type][i] === fn){ this.messages[type].splice(i, 1); break; } } } }, }
發佈者(經過emit發佈函數)須要傳遞參數給訂閱者(的回調函數),能夠將須要傳遞的參數,以一個對象的形式,做爲emit函數的第二個參數,也能夠(在emit發佈函內部)經過arguments對象獲取。數組
var observer = { events: {}, listen: function(type, cb){ if(!this.events[type]){ this.events[type] = []; } this.events[type].push(cb); }, emit: function(type, data){ // let type = Array.prototype.shift.call(arguments); // let args = arguments; if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){ for(let i = 0, task; task = this.messages[type][i++];){ task.call(null, data); // task.apply(null, args); } } }, remove: function(type, fn){ if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){ for(let i = this.messages[type].length - 1;i>=0;i--){ if(this.messages[type][i] === fn){ this.messages[type].splice(i, 1); break; } } } }, }
訂閱者也能夠給本身的回調函數綁定this服務器
var observer = { events: {}, listen: function(type, cb, self){ if(!this.events[type]){ this.events[type] = []; } this.events[type].push([cb, self]); }, emit: function(type, data){ // let type = Array.prototype.shift.call(arguments); // let args = arguments; if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){ for(let i = 0, task; task = this.messages[type][i++];){ task[0].call(task[1], data); // task[0].apply(task[1], args); } } }, remove: function(type, fn){ if(Array.isArray(this.messages[type]) && this.messages[type].length > 0){ for(let i = this.messages[type].length - 1;i>=0;i--){ if(this.messages[type][i][0] === fn){ this.messages[type].splice(i, 1); break; } } } }, }
能夠將訂閱,發佈,以及取消訂閱三個方法返回this,來實現觀察者對象的鏈式調用,由於都是經過觀察者來調用這個三個方法,所以這三個方法的this都是這個觀察者。閉包
var observer = { events: {}, listen: function(){ ... return this; }, emit: function(){ ... return this; }, remove: function(){ ... return this; }, }
由於觀察者本質上是一個對象,所以咱們能夠把它封裝成一個類(構造函數或es6的class);也能夠把它單獨放在一個文件裏,而後導出一個對象(注意events做爲私有屬性,只導出三個方法便可);還能夠做爲一個IIFE(閉包),導出一個對象(注意events做爲私有屬性,只導出三個方法便可)。app
class Observer { constructor() { this.events = {}; } listen(){} emit(){} remove(){} }
var new Observer() { this.events = {}; } Observer.prototype.listen = function(){}; Observer.prototype.emit = function(){}; Observer.prototype.remove = function(){};
// event.js文件 let events = {}; function listen(){}; function emit(){}; function remove(){}; export.listen; export.emit; export.remove;
// 將觀察者放在閉包中,當頁面加載就當即執行 var observer = (function(){ // 防止消息隊列暴露而被串改,故將消息容器做爲靜態私有變量保存 let events = {}; return { listen: function(){}, emit: function(){}, remove: function(){}, }; })()
咱們所瞭解到的發佈—訂閱模式,都是訂閱者必須先訂閱一個消息,隨後才能接收到發佈者 發佈的消息。若是把順序反過來,發佈者先發布一條消息,而在此以前並無對象來訂閱它,這 條消息無疑將消失在宇宙中。異步
在某些狀況下,咱們須要先將這條消息保存下來,等到有對象來訂閱它的時候,再從新把消 息發佈給訂閱者。就如同 QQ 中的離線消息同樣,離線消息被保存在服務器中,接收人下次登陸 上線以後,能夠從新收到這條消息。模塊化
這種需求在實際項目中是存在的,好比在以前的商城網站中,獲取到用戶信息以後才能渲染 用戶導航模塊,而獲取用戶信息的操做是一個 ajax 異步請求。當 ajax 請求成功返回以後會發布 一個事件,在此以前訂閱了此事件的用戶導航模塊能夠接收到這些用戶信息。
可是這只是理想的情況,由於異步的緣由,咱們不能保證 ajax 請求返回的時間,有時候它返 回得比較快,而此時用戶導航模塊的代碼尚未加載好(尚未訂閱相應事件),特別是在用了 一些模塊化惰性加載的技術後,這是極可能發生的事情。也許咱們還須要一個方案,使得咱們的 發佈—訂閱對象擁有先發布後訂閱的能力。
爲了知足這個需求,咱們要創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者來訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裝函數將被存入堆棧中,等到終於有對象來訂閱此事件的時候,咱們將遍歷堆棧而且依次執行這些包裝函數, 也就是從新發布里面的事件。固然離線事件的生命週期只有一次,就像 QQ 的未讀消息只會被重 新閱讀一次,因此剛纔的操做咱們只能進行一次。
咱們須要實現這樣的邏輯
observer.emit('click', {a:1}); observer.listen('click',(args)=>{console.log(args)}) // {a:1}
目標明確後, 來着手實現它:
var observer = { events: {}, cacheList: [], listen: function(type, cb){ if (!this.events[type]) { this.events[type] = []; } this.events[type].push(cb) for (let i = 0; i < this.cacheList.length; i++) { this.cacheList[i](); } }, emit: function(type, data){ const that = this function cache() { let arr = that.events[type]; for (let i = 0; i < arr.length; i++) { arr[i].call(arr[i], data); } } this.cacheList.push(cache) }, }
存疑:保證只執行了一次?