[轉] 淺析JavaScript設計模式——發布-訂閱/觀察者模式
阿新 • • 發佈:2018-04-06
div 函數 ons class esp 了解 寵物 是我 ring
前一段時間一直在寫CSS3的文章
一直都沒寫設計模式
今天來寫寫大名鼎鼎觀察者模式
先畫張圖
觀察者模式的理解
我覺得還是發布-訂閱模式的叫法更容易我們理解
(不過也有的書上認為它們是兩種模式……)
這就類似我們在微信平臺訂閱了公眾號
當它有新的文章發表後,就會推送給我們所有訂閱的人
我們可以看到例子中這種模式的優點
- 我們作為訂閱者不必每次都去查看這個公眾號有沒有新文章發布,
公眾號作為發布者會在合適時間通知我們 - 我們與公眾號之間不再強耦合在一起。公眾號不關心誰訂閱了它,
不管你是男是女還是寵物狗,它只需要定時向所有訂閱者發布消息即可
很簡單的道理,過年的時候,群發祝福短信一定要比挨個發短信方便的多
通過上面的例子映射出我們觀察者模式的優點
- 可以廣泛應用於異步編程,它可以代替我們傳統的回調函數
我們不需要關註對象在異步執行階段的內部狀態,我們只關心事件完成的時間點 - 取代對象之間硬編碼通知機制,一個對象不必顯式調用另一個對象的接口,而是松耦合的聯系在一起
雖然不知道彼此的細節,但不影響相互通信。更重要的是,其中一個對象改變不會影響另一個對象
可能看完這些一臉懵逼,不過沒關系
下面我們會深入體會它的優點
自定義事件
其實觀察者模式我們都曾使用過,就是我們熟悉的事件
但是內置的事件很多時候不能滿足我們的要求
所以我們需要自定義事件
現在我們想實現這樣的功能
定義一個事件對象,它有以下功能
- 監聽事件(訂閱公眾號)
- 觸發事件(公眾號發布)
- 移除事件(取訂公眾號)
當然我們不可能只訂閱一個公眾號,可能會有很多
所以我們要針對不同的事件設置不同的”鍵”
這樣我們儲存事件的結構應該是這樣的
//偽代碼
Event = {
name1: [回調函數1,回調函數2,...],
name2: [回調函數1,回調函數2,...],
name3: [回調函數1,回調函數2,...],
}
- 1
- 2
- 3
- 4
- 5
- 6
代碼如下
var Event = (function(){
var list = {},
listen,
trigger,
remove;
listen = function(key,fn){ //監聽事件函數
if(!list[key]){
list[key] = []; //如果事件列表中還沒有key值命名空間,創建
}
list[key].push(fn); //將回調函數推入對象的“鍵”對應的“值”回調數組
};
trigger = function(){ //觸發事件函數
var key = Array.prototype.shift.call(arguments); //第一個參數指定“鍵”
msg = list[key];
if(!msg || msg.length === 0){
return false; //如果回調數組不存在或為空則返回false
}
for(var i = 0; i < msg.length; i++){
msg[i].apply(this, arguments); //循環回調數組執行回調函數
}
};
remove = function(key, fn){ //移除事件函數
var msg = list[key];
if(!msg){
return false; //事件不存在直接返回false
}
if(!fn){
delete list[key]; //如果沒有後續參數,則刪除整個回調數組
}else{
for(var i = 0; i < msg.length; i++){
if(fn === msg[i]){
msg.splice(i, 1); //刪除特定回調數組中的回調函數
}
}
}
};
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
var fn = function(data){
console.log(data + ‘的推送消息:xxxxxx......‘);
}
Event.listen(‘某公眾號‘, fn);
Event.trigger(‘某公眾號‘, ‘2016.11.26‘);
Event.remove(‘某公眾號‘, fn);
通過這種全局的Event對象,我們可以利用它在兩個模塊間實現通信
並且兩個模塊互不幹擾
//偽代碼
module1 = function(){
Event.listen(...);
}
module2 = function(){
Event.trigger(...);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
完整的觀察者對象
我們上面寫的Event對象還是存在一些問題
- 只能先“訂閱”再“發布”
- 全局對象會產生命名沖突
對於第一點,在我們訂閱了一個公眾號之前,它是同樣會定時推送消息的
我們訂閱後,同樣可以查看歷史推送
所以我們應該是實現先發布再訂閱仍然可以查看歷史消息
對於第二點,如果我們每個人都使用上面我們定義的函數
所有事件都放在一個list當中
時間長了一定會產生命名沖突
最好的辦法就是為對象增加創建命名空間的功能
現在我們來豐滿我們的事件對象
(這裏我就盜用大神寫的代碼了,不同的人不同庫實現的方式都不同)
這個完整版稍微了解一下就好了
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(){
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.trigger(‘click‘,1);
Event.listen(‘click‘,function(a){
console.log(a); //1
});
/********* 使用命名空間 *********/
Event.create(‘namespace1‘).listen(‘click‘,function(a){
console.log(a); //1
})
Event.create(‘namespace1‘).trigger(‘click‘,1);
Event.create(‘namespace3‘).listen(‘click‘,function(a){
console.log(a); //2
})
Event.create(‘namespace3‘).trigger(‘click‘,2);
總結
觀察者模式有兩個明顯的優點
- 時間上解耦
- 對象間解耦
它應用廣泛,但是也有缺點
創建這個函數同樣需要內存,過度使用會導致難以跟蹤維護
[轉] 淺析JavaScript設計模式——發布-訂閱/觀察者模式