簡介 觀察者模式又叫發佈 訂閱模式,它定義了對象間的一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,當一個對象發生改變時,所有依賴於它的對象都將得到通知。 舉一個現實生活中的例子,例如小紅在淘寶的一家店裡看上了一雙紅色的鞋,小李也在這家店裡面看上了一頂黑色的帽子,但是聯繫賣家時,賣家回答這 ...
簡介
觀察者模式又叫發佈---訂閱模式,它定義了對象間的一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,當一個對象發生改變時,所有依賴於它的對象都將得到通知。
舉一個現實生活中的例子,例如小紅在淘寶的一家店裡看上了一雙紅色的鞋,小李也在這家店裡面看上了一頂黑色的帽子,但是聯繫賣家時,賣家回答這兩樣都沒貨了。賣家告訴小紅小李,要是喜歡的話,可以關註下店鋪,到貨了,我會給大家通知的。這就是一個典型的發佈-訂閱模式,賣家是發佈者,買家是訂閱者。當貨來的時候,會依次通知小紅、小李等買家,依次給他們發消息通知。
代碼實現
var goodsObj = {}; goodsObj.list = []; //存放訂閱者 //訂閱者 goodsObj.subscribe = function(key,fn){ if(!this.list[key]){ this.list[key] = []; } //訂閱的消息存放到緩存列表中 this.list[key].push(fn); } //取消訂閱 goodsObj.unsubscribe = function(key,fn){ var fns = this.list[key]; //如果key對應的消息沒有訂閱過的話,則返回 if(!fns){ return false; } //如果沒有傳入具體的回調函數,表示取消key對應消息的所有訂閱 if(!fn){ fns && (fns.length = 0); }else{ for(var i = fns.length - 1; i >= 0; i--){ if(fns[i] === fn){ //刪除訂閱者的回調函數 fns.splice(i,1); } } } } //發佈者 goodsObj.publish = function(){ var key = Array.prototype.shift.call(arguments); var fns = this.list[key]; //如果沒有訂閱過該key的消息,直接返回 if(!fns || fns.length === 0){ return false; } for(var i = 0,fn; fn = fns[i++];){ fn.apply(this,arguments); } } //小紅訂閱了鞋 goodsObj.subscribe('shoes',fn1 = function(color){ console.log('訂閱鞋的顏色:' + color + '到貨了,可以拍了'); }); //小李訂閱了帽子 goodsObj.subscribe('cap',fn2 = function(size){ console.log('訂閱帽子的尺寸:' + size + '到貨了,可以拍了'); }); //發佈 goodsObj.publish('shoes','red'); goodsObj.publish('cap',40); //取消訂閱 goodsObj.unsubscribe('shoes',fn1); //發佈消息,看是否能收到 goodsObj.publish('shoes','block');
優化取消訂閱
為了優化取消訂閱,在訂閱的時候,給每個訂閱者一個不同的標識,代碼如下:
var goodsObj = {}; goodsObj.list = {}; //存放訂閱者 var subuid = -1; //每個訂閱者標識不一樣 //訂閱者 goodsObj.subscribe = function(key,fn){ if(!this.list[key]){ this.list[key] = []; } //訂閱的消息存放到緩存列表中 var token= (++subuid).toString(); this.list[key].push({ token:token, fn:fn }); return token; } //取消訂閱 goodsObj.unsubscribe = function(token){ for(var m in goodsObj.list){ if(goodsObj.list.hasOwnProperty(m)){ for(var i = 0, j = goodsObj.list[m].length; i < j; i++ ){ if(goodsObj.list[m][i].token == token){ goodsObj.list[m].splice(i,1); break; } } } } } //發佈者 goodsObj.publish = function(){ var key = Array.prototype.shift.call(arguments); var fns = this.list[key]; //如果沒有訂閱過該key的消息,直接返回 if(!fns || fns.length === 0){ return false; } for(var i = 0,obj; obj = fns[i++];){ obj.fn.apply(this,arguments); } } //小紅訂閱了鞋 var token1 = goodsObj.subscribe('shoes',function(color){ console.log('訂閱鞋的顏色:' + color + '到貨了,可以拍了'); }); //小李訂閱了帽子 goodsObj.subscribe('cap',function(size){ console.log('訂閱帽子的尺寸:' + size + '到貨了,可以拍了'); }); //發佈 goodsObj.publish('shoes','red'); goodsObj.publish('cap',40); //取消訂閱 goodsObj.unsubscribe(token1); //發佈消息,看是否能收到 goodsObj.publish('shoes','block');
封裝全局發佈訂閱對象
封裝一個全局的發佈-訂閱對象,代碼如下:
var Event = (function(){ var list = {}, //存放訂閱者 subscribe, unsubscribe, publish, subuid = -1; //每個訂閱者標識不一樣 //訂閱者 subscribe = function(key,fn){ if(!list[key]){ list[key] = []; } //訂閱的消息存放到緩存列表中 var token= (++subuid).toString(); list[key].push({ token:token, fn:fn }); return token; }; //取消訂閱 unsubscribe = function(token){ for(var m in list){ if(list.hasOwnProperty(m)){ for(var i = 0, j = list[m].length; i < j; i++ ){ if(list[m][i].token == token){ list[m].splice(i,1); break; } } } } }; //發佈者 publish = function(){ var key = Array.prototype.shift.call(arguments); var fns = list[key]; //如果沒有訂閱過該key的消息,直接返回 if(!fns || fns.length === 0){ return false; } for(var i = 0,obj; obj = fns[i++];){ obj.fn.apply(this,arguments); } }; return { publish:publish, subscribe:subscribe, unsubscribe:unsubscribe } })(); //小紅訂閱了鞋 var token1 = Event.subscribe('shoes',function(color){ console.log('訂閱鞋的顏色:' + color + '到貨了,可以拍了'); }); //小李訂閱了帽子 Event.subscribe('cap',function(size){ console.log('訂閱帽子的尺寸:' + size + '到貨了,可以拍了'); }); //發佈 Event.publish('shoes','red'); Event.publish('cap',40); //取消訂閱 Event.unsubscribe(token1); //發佈消息,看是否能收到 Event.publish('shoes','block');
實例
我們使用上面封裝的全局的發佈-訂閱對象來實現兩個模塊之間的通信問題;比如現在有一個頁面有一個按鈕,每次點擊此按鈕後,div中會顯示此按鈕被點擊的總次數;如下代碼:
<button id="clickBtn">點擊我計數</button> <div id="showCount">0</div>
JS代碼如下:
(function(){ var count = 0; //發佈(責處理點擊操作、發佈消息) document.getElementById('clickBtn').addEventListener('click',function(){ Event.publish('add',count++); },false); //訂閱(負責監聽add這個消息,並把點擊的總次數顯示到頁面上來) Event.subscribe('add',function(curCount){ document.getElementById('showCount').innerHTML = curCount; }) })();
訂閱發佈更精簡代碼:
var Event = function(){ var listen,obj,remove,one,trigger,__this; obj = {}; __this = this; listen = function(key,eventfn){ var _ref,stack; stack = (_ref = obj[key]) != null ? _ref : obj[key] = []; return stack.push(eventfn); }; one = function(key,eventfn){ remove(key); return listen(key,eventfn); }; remove = function(key){ var _ref; return (_ref = obj[key]) != null ? _ref.length = 0 : void 0; }; trigger = function(){ var fn,stack,_i,_len,_ref,key; key = Array.prototype.shift.call(arguments); stack = (_ref = obj[key]) != null ? _ref :obj[key] = []; for(_i=0,_len=stack.length;_i<_len;_i++){ fn = stack[_i]; if(fn.apply(__this,arguments) === false){ return false; } } } return { listen:listen, one:one, remove:remove, trigger:trigger } } var addTv = Event(); addTv.listen('play',function(data){ console.log('今天上午要播放的電影是:' +data.name); }); addTv.one('play',function(data){ console.log('只有我訂閱的電影信息:' +data.name); }); addTv.trigger('play',{'name':'國產凌凌漆'}); addTv.remove('play'); addTv.trigger('play',{'name':'國產凌凌漆22'});