淺析JavaScript設計模式——釋出-訂閱/觀察者模式
觀察者模式
定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知
前一段時間一直在寫CSS3的文章
一直都沒寫設計模式
今天來寫寫大名鼎鼎觀察者模式
先畫張圖
觀察者模式的理解
我覺得還是釋出-訂閱模式的叫法更容易我們理解
(不過也有的書上認為它們是兩種模式……)
這就類似我們在微信平臺訂閱了公眾號
當它有新的文章發表後,就會推送給我們所有訂閱的人
我們可以看到例子中這種模式的優點
- 我們作為訂閱者不必每次都去檢視這個公眾號有沒有新文章釋出,
公眾號作為釋出者會在合適時間通知我們 - 我們與公眾號之間不再強耦合在一起。公眾號不關心誰訂閱了它,
不管你是男是女還是寵物狗,它只需要定時向所有訂閱者釋出訊息即可
很簡單的道理,過年的時候,群發祝福簡訊一定要比挨個發簡訊方便的多
通過上面的例子映射出我們觀察者模式的優點
- 可以廣泛應用於非同步程式設計,它可以代替我們傳統的回撥函式
我們不需要關注物件在非同步執行階段的內部狀態,我們只關心事件完成的時間點 - 取代物件之間硬編碼通知機制,一個物件不必顯式呼叫另一個物件的介面,而是鬆耦合的聯絡在一起
雖然不知道彼此的細節,但不影響相互通訊。更重要的是,其中一個物件改變不會影響另一個物件
可能看完這些一臉懵逼,不過沒關係
下面我們會深入體會它的優點
自定義事件
其實觀察者模式我們都曾使用過,就是我們熟悉的事件
但是內建的事件很多時候不能滿足我們的要求
所以我們需要自定義事件
現在我們想實現這樣的功能
定義一個事件物件,它有以下功能
- 監聽事件(訂閱公眾號)
- 觸發事件(公眾號釋出)
- 移除事件(取訂公眾號)
當然我們不可能只訂閱一個公眾號,可能會有很多
所以我們要針對不同的事件設定不同的”鍵”
這樣我們儲存事件的結構應該是這樣的
//虛擬碼
Event = {
name1: [回撥函式1,回撥函式2,...],
name2: [回撥函式1,回撥函式2,...],
name3: [回撥函式1,回撥函式2,...],
}
程式碼如下
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(...);
}
完整的觀察者物件
我們上面寫的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);
總結
觀察者模式有兩個明顯的優點
- 時間上解耦
- 物件間解耦
它應用廣泛,但是也有缺點
建立這個函式同樣需要記憶體,過度使用會導致難以跟蹤維護