發佈—訂閱模式讓兩個對象鬆耦合地聯繫在一塊兒,雖然不太清楚彼此的細節,但這不影響它們之間相互通訊。當有新的訂閱者出現時,發佈者的代碼不須要任何修改;一樣發佈者須要改變時,也不會影響到以前的訂閱者。只要以前約定的事件名沒有變化,就能夠自由地改變它們。javascript
var salesOffices = {}; // 定義售樓處 salesOffices.clientList = []; // 緩存列表,存放訂閱者的回調函數 salesOffices.listen = function (fn) { // 增長訂閱者 this.clientList.push(fn); // 訂閱的消息添加進緩存列表 }; salesOffices.trigger = function () { // 發佈消息 for (var i = 0, fn; fn = this.clientList[i++];) { fn.apply(this, arguments); // (2) // arguments 是發佈消息時帶上的參數 } }; salesOffices.listen(function (price, squareMeter) { // 小明訂閱消息 console.log('價格= ' + price); console.log('squareMeter= ' + squareMeter); }); salesOffices.listen(function (price, squareMeter) { // 小紅訂閱消息 console.log('價格= ' + price); console.log('squareMeter= ' + squareMeter); }); salesOffices.trigger(2000000, 88); // 輸出:200 萬,88 平方米 salesOffices.trigger(3000000, 110); // 輸出:300 萬,110 平方米
上面的代碼仍是有點問題咱們看到訂閱者接收到了發佈者發佈的每一個消息,雖然小明只想買88 平方米的房子,可是發佈者把110 平方米的信息也推送給了小明,這對小明來講是沒必要要的困擾。因此咱們有必要增長一個標示key,讓訂閱者只訂閱本身感興趣的消息。優化代碼爲:html
var salesOffices = {}; // 定義售樓處 salesOffices.clientList = {}; // 緩存列表,存放訂閱者的回調函數 salesOffices.listen = function (key, fn) { if (!this.clientList[key]) { // 若是尚未訂閱過此類消息,給該類消息建立一個緩存列表 this.clientList[key] = []; } this.clientList[key].push(fn); // 訂閱的消息添加進消息緩存列表 }; salesOffices.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); // (2) // arguments 是發佈消息時附送的參數 } }; salesOffices.listen('squareMeter88', function (price) { // 小明訂閱88 平方米房子的消息 console.log('價格= ' + price); // 輸出: 2000000 }); salesOffices.listen('squareMeter110', function (price) { // 小紅訂閱110 平方米房子的消息 console.log('價格= ' + price); // 輸出: 3000000 }); salesOffices.trigger('squareMeter88', 2000000); // 發佈88 平方米房子的價格 salesOffices.trigger('squareMeter110', 3000000); // 發佈110 平方米房子的價格
var event = { clientList: [], 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), // (1); fns = this.clientList[key]; if (!fns || fns.length === 0) { // 若是沒有綁定對應的消息 return false; } for (var i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments); // (2) // arguments 是trigger 時帶上的參數 } } }; var installEvent = function (obj) { for (var i in event) { obj[i] = event[i]; } }; var salesOffices = {}; installEvent(salesOffices); salesOffices.listen('squareMeter88', function (price) { // 小明訂閱消息 console.log('價格= ' + price); }); salesOffices.listen('squareMeter100', function (price) { // 小紅訂閱消息 console.log('價格= ' + price); }); salesOffices.trigger('squareMeter88', 2000000); // 輸出:2000000 salesOffices.trigger('squareMeter100', 3000000); // 輸出:3000000
event.remove = function (key, fn) { var fns = this.clientList[key]; if (!fns) { // 若是key 對應的消息沒有被人訂閱,則直接返回 return false; } if (!fn) { // 若是沒有傳入具體的回調函數,表示須要取消key 對應消息的全部訂閱 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('價格= ' + price); }); salesOffices.listen('squareMeter88', fn2 = function (price) { // 小紅訂閱消息 console.log('價格= ' + price); }); salesOffices.remove('squareMeter88', fn1); // 刪除小明的訂閱 salesOffices.trigger('squareMeter88', 2000000); // 輸出:2000000
假如咱們正在開發一個商城網站,網站裏有header 頭部、nav 導航、消息列表、購物車等模塊。這幾個模塊的渲染有一個共同的前提條件,就是必須先用ajax 異步請求獲取用戶的登陸信息。但如今還不足以說服咱們在此使用發佈—訂閱模式,由於異步的問題一般也能夠用回調函數來解決。更重要的一點是,咱們不知道除了header 頭部、nav 導航、消息列表、購物車以外,未來還有哪些模塊須要使用這些用戶信息,須要發佈—訂閱模式方便後期擴展。java
var login = { clientList: [], 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), // (1); fns = this.clientList[key]; if (!fns || fns.length === 0) { // 若是沒有綁定對應的消息 return false; } for (var i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments); // (2) // arguments 是trigger 時帶上的參數 } } }; $.ajax('http:// xxx.com?login', function (data) { // 登陸成功 login.trigger('loginSucc', data); // 發佈登陸成功的消息 }); var header = (function () { // header 模塊 login.listen('loginSucc', function (data) { header.setAvatar(data.avatar); }); return { setAvatar: function (data) { console.log('設置header 模塊的頭像'); } } })(); var nav = (function () { // nav 模塊 login.listen('loginSucc', function (data) { nav.setAvatar(data.avatar); }); return { setAvatar: function (avatar) { console.log('設置nav 模塊的頭像'); } } })(); var address = (function () { // nav 模塊 login.listen('loginSucc', function (obj) { address.refresh(obj); }); return { refresh: function (avatar) { console.log('刷新收貨地址列表'); } } })();
實現發佈—訂閱模式例子中還有兩個問題:ajax
發佈—訂閱模式能夠用一個全局的Event 對象來實現,訂閱者不須要了解消息來自哪一個發佈者,發佈者也不知道消息會推送給哪些訂閱者,Event 做爲一個相似「中介者」的角色,把訂閱者和發佈者聯繫起來。編程
var Event = (function () { var clientList = {}, listen, 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: listen, trigger: trigger, remove: remove } })(); Event.listen('squareMeter88', function (price) { // 小紅訂閱消息 console.log('價格= ' + price); // 輸出:'價格=2000000' }); Event.trigger('squareMeter88', 2000000); // 售樓處發佈消息
基於一個全局的Event 對象實現發佈—訂閱模式,咱們利用它能夠在兩個封裝良好的模塊中進行通訊,這兩個模塊能夠徹底不知道對方的存在。好比如今有兩個模塊,a 模塊裏面有一個按鈕,每次點擊按鈕以後,b 模塊裏的div 中會顯示按鈕的總點擊次數,咱們用全局發佈—訂閱模式完成下面的代碼,使得a 模塊和b 模塊能夠在保持封裝性的前提下進行通訊。緩存
<!DOCTYPE html> <html> <body> <button id="count">點我</button> <div id="show"></div> <script type="text/JavaScript"> var a = (function(){ var count = 0; var button = document.getElementById( 'count' ); button.onclick = function(){ Event.trigger( 'add', count++ ); } })(); var b = (function(){ var div = document.getElementById( 'show' ); Event.listen( 'add', function( count ){ div.innerHTML = count; }); })(); </script> </body> </html>
爲了知足這個需求,咱們要創建一個存放離線事件的堆棧,當事件發佈的時候,若是此時尚未訂閱者來訂閱這個事件,咱們暫時把發佈事件的動做包裹在一個函數裏,這些包裝函數將被存入堆棧中,等到終於有對象來訂閱此事件的時候,咱們將遍歷堆棧而且依次執行這些包裝函數,也就是從新發布里面的事件。固然離線事件的生命週期只有一次,就像QQ 的未讀消息只會被從新閱讀一次,因此剛纔的操做咱們只能進行一次。服務器
var Event = (function () { var global = this, Event, _default = 'default'; Event = function () { var _listen, _trigger, _remove, _slice = Array.prototype.slice, _shift = Array.prototype.shift, _unshift = Array.prototype.unshift, namespaceCache = {}, _create, find, each = function (ary, fn) { var ret; for (var i = 0, l = ary.length; i < l; i++) { var n = ary[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; i >= 0; i--) { if (cache[key][i] === fn) { cache[key].splice(i, 1); } } } else { cache[key] = []; } } }; _trigger = function () { var cache = _shift.call(arguments), key = _shift.call(arguments), args = arguments, _self = this, ret, stack = cache[key]; if (!stack || !stack.length) { return; } return each(stack, function () { return this.apply(_self, args); }); }; _create = function (namespace) { var namespace = namespace || _default; var cache = {}, offlineStack = [], // 離線事件 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, _self = this; _unshift.call(arguments, cache); args = arguments; fn = function () { return _trigger.apply(_self, 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; })(); Event.create('namespace1').listen('click', function (a) { console.log(a); // 輸出:1 }); Event.create('namespace1').trigger('click', 1); Event.create('namespace2').listen('click', function (a) { console.log(a); // 輸出:2 }); Event.create('namespace2').trigger('click', 2);