發佈-訂閱模式又叫觀察者模式,它定義與對象間的一種一對多的關係,當一個對象的狀態發生改變時,全部依賴與它的對象都將獲得通知。
發佈-訂閱模式能夠普遍應用與異步編程中,這是一種替代傳統回調函數的方案。ajax
發佈-訂閱能夠取代對象之間硬編碼的通訊機制,一個對象再也不顯式地調用另一個對象的某個接口。編程
var event = { //緩存列表,存放訂閱者的回調函數 clientList: [], //增長訂閱者, key:事件名,fn:回調函數 listen: function(key, fn){ if(!this.clientList[key]){ this.clientList[key] = [] } this.clientList[key].push(fn) }, //發佈消息 trigger: function(){ // 取出消息類型 var key = Array.prototype.shift.call(arguments) // 取出消息類型對應的回調函數集合 fns = this.clientList[key] // 若是沒有訂閱該消息,則返回 if(!fns || fns.length === 0) { return false } for(var i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments) } }, //取消訂閱 remove: function(key, fn) { var fns = this.clientList[key] // 若是對應的消息沒人訂閱,直接返回 if(!fns) { return false } // 若是沒有傳入回調函數,表示取消key對應消息的全部訂閱 if(!fn){ fns && (fns.length = 0) } else { for(var l = fns.length - 1; l >= 0; l--) { var _fn = fns[l] if (_fn === fn) { fns.splice(l, 1) } } } } } var salesOffices = {} // 給對象安裝發佈-訂閱功能 var installEvent = function(obj){ for(var i in event){ obj[i] = event[i] } } installEvent(salesOffices) salesOffices.listen('squareMeter88', fn1 = function(price){ console.log('價格1:'+price) }) salesOffices.listen('squareMeter87', fn2 = function(price){ console.log('價格2:'+price) }) salesOffices.trigger('squareMeter88', 20000) // 價格1: 20000 salesOffices.remove('squareMeter88') salesOffices.trigger('squareMeter88', fn1) // 打印結果爲空
其實在這個發佈-訂閱模式中還存在兩個問題:緩存
salesOffices
這個對象因此接下來實現一個全局的發佈-訂閱對象服務器
var Event = (function(){ var clientList = {}, listten, trigger, remove; listen = function(key, fn){ if(!clientList[key]){ clientList[key] = [] } clientList[key].push(fn) } trigger = function(){ var key = Array.prototype.shift.call(arguments) fns = clientList[key] if(!fns || fns.length === 0) { return false } for(var i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments) } }, remove = function(key, fn) { var fns = clientList[key] if(!fns) { return false } if(!fn){ fns && (fns.length = 0) } else { for(var l = fns.length - 1; l >= 0; l--) { var _fn = fns[l] if (_fn === fn) { fns.splice(l, 1) } } } } return { listen, trigger, remove } })() Event.listen('squareMeter88', fn1 = function(price){ // 訂閱消息 console.log('價格1:'+price) }) Event.trigger('squareMeter88', 20000) // 價格1: 20000
咱們所瞭解的訂閱發佈都是訂閱者必須先訂閱一個消息,隨後才能接受到發佈者發佈的消息。若是順序反過來,發佈者先發布一條消息,而此時沒有訂閱者,無疑消息會消失。架構
在某些狀況下,咱們須要先講這條消息保存下來,等到有對象訂閱它的時候,再從新把消息發佈給訂閱者,若是QQ中的離線消息,離線消息先被保存在服務器中,接收人下次登錄以後,能夠重新接收到這條消息。app
再假如一個商城系統中,獲取到用戶信息後才能渲染導航模塊,無疑這是一個異步的過程,不能保證ajax什麼時候返回。因此咱們須要咱們發佈訂閱對象擁有先發布後訂閱的能力。異步
爲了知足這種須要,咱們要創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者來訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裹函數被存入堆棧中,等到終於有對象來訂閱此事件,咱們將遍歷堆棧而且依次執行這些包裝函數,也就是從新發布里面的事件。固然離線事件的生命週期只有一次,就像QQ未讀消息只會被從新閱讀一次。異步編程
全局的訂閱-發佈對象裏只有一個clientList來存放消息名和回調函數,咱們都經過它來訂閱和發佈各類消息,長此以往,不免會出現事件名衝突的狀況,因此須要給Event對象提供建立命名空間的功能。函數
具體實現
var Event = (function() { var Event, _default = 'default'; Event = function() { var _listen, _trigger, _remove, _shift = [].shift, _unshift = [].unshift, namespaceCache = {}, _create, each = function(arr, fn) { var ret; for (var i = 0, l = arr.length; i < l; i++) { var n = arr[i]; ret = fn.call(n, i, n); } return ret; }; _listen = function(key, fn, cache) { if (!cache[key]) { cache[key] = []; } cache[key].push(fn); }; _remove = function(key, cache, fn) { if (cache[key]) { if (fn) { for (var i = cache[key].length - 1; i >= 0; i--) { if (cache[key][i] === fn) { // 刪除訂閱回調函數 cache[key].splice(i, 1); } } } else { cache[key] = []; } } }; _trigger = function() { var cache = _shift.call(arguments); var key = _shift.call(arguments); var args = arguments; var _this = this; var stack = cache[key]; if (!stack || !stack.length) { return; } return each(stack, function() { return this.apply(_this, args); }) }; _create = function(namespace) { namespace = namespace || _default; var cache = {}; var offlineStack = []; //離線事件 var ret = { listen: function(key, fn, last) { _listen(key, fn, cache); if (offlineStack === null) { return; } if (last === 'last') { offlineStack.length && offlineStack.pop()(); } else { each(offlineStack, function() { this(); }); } offlineStack = null; }, one: function(key, fn, last) { _remove(key, cache); this.listen(key, fn, last); }, remove: function(key, fn) { _remove(key, cache, fn); }, trigger: function() { var fn, args, _this = this; _unshift.call(arguments, cache); args = arguments; fn = function() { return _trigger.apply(_this, args); }; if (offlineStack) { return offlineStack.push(fn); } return fn(); } }; return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret; }; return { create: _create, one: function(key, fn, last) { var event = this.create(); event.one(key, fn, last); }, remove: function(key, fn) { var event = this.create(); event.remove(key, fn); }, listen: function(key, fn, last) { var event = this.create(); event.listen(key, fn, last); }, trigger: function() { var event = this.create(); event.trigger.apply(this, arguments); } } }(); return Event; }()); var fn1 = function(price) { console.log(price); }; // 實例 Event.listen('squareMeter88', fn1); Event.remove('squareMeter88', fn1); Event.listen('squareMeter88', function(price) { console.log('fn2: ' + price); }); Event.trigger('squareMeter88', 20000); // fn2: 20000
時間與對象之間的解耦,應用很是普遍,既能夠用在異步編程中,也能夠幫助咱們完成更鬆耦合的代碼編寫,從架構上看,不管MVC仍是MVVM都少不了發佈-訂閱的參與this
建立訂閱者自己要消耗必定時間和內存,不管消息是否發生,一直存在內存中。雖然發佈-訂閱模式雖然能夠弱化對象之間的聯繫,可是過分使用,對象之間的必然聯繫會被深埋,特別是多個訂閱者和發佈者嵌套會形成難以追蹤的bug。