[TOC]
# 發布-訂閱模式
又稱為觀察者模式,它定義對象間的一種一對多的依賴關系,當一個對象的狀態發生
改變的時,所有依賴于它的對象都將得到通知。一為時間上解耦,二為對象之間的解
耦。
## DOM事件
DOM元素上的事件函數,就是如此。當我們為一個元素增加一個事件時,等到這個時
間觸發后,就會執行我們的回調函數,同時也能夠移除該事件。
## 自定義事件
發布-訂閱模式的通用實現
```javascript
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),
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 || fns.length === 0){
return false;
}
if(!fn){
fns && (fns.lengthss = 0); // 如果沒有傳 具的回調函數,表示需要取消key對應消息的所有訂閱
}else{
for(var l = fns.length - 1; l >=0; l--){ // 反向遍歷
var _fn = fns[l];
if(_fn === fn){
fns.splice(l, 1); // 刪除訂閱者的回調函數
}
}
}
}
};
// 再定義 個installEvent函數,這個函數可以給所有的對象都動態安裝發布-訂閱功能
var installEvent = function(obj){
for(var i in event){
event[i] = obj[i];
}
};
var salesOffices = { ... };
installEvent(salesOffices);
salesOffices.listen('event1');
salesOffices.trigger('event1');
salesOffices.remove('event1');
```
## 全局的發布-訂閱對象
發布-訂閱模式可以用一個全局的Event對象來實現,訂閱者不需要了解消息來自哪個 發布者,發布者也不知道消息會推送給哪些訂閱者,Event作為一個類似“中介 者”的角色,把訂閱者和發布者聯系起來。
```javascript
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 || fns.length === 0){
return false;
}
if(!fn){
fns && (fns.lengthss = 0); // 如果沒有傳 具的回調函數,表示需要取消key對應消息的所有訂閱
}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('event1');
Event.trigger('event1');
Event.remove('event1');
```
## 模塊間通信
模塊之間如果用了太多的全局發布-訂閱模式來通信,那么模塊與模塊之間的聯系就 被隱藏到了背后,我們最終會搞不清楚消息來自哪個模塊,或者消息會流向哪些模 塊,這又會給我們的維護帶來一些麻煩,也許某個模塊的作用就是暴露一些接口給其 他模塊調用。
## 必須先訂閱再發布嗎
在某些情況下,需要先將消息保存下來,等到有對象來訂閱它的時候,再重新把消息
發布給訂閱者。就如果離線消息一樣。
我們需要建立一個存放離線事件的堆棧,當事件發布的時候,如果此時還沒有訂閱者
來訂閱這個事件,我們暫時把發布事件的動作包裹在一個函數里,這些包裝函數
存入堆棧中,等到終于有對象來訂閱此事件的時候,我們將遍歷堆棧并且依次執行這
些包裝函數,也就是重新發布里面的事件。當然離線事件的生命周期只有一次。
## 全局事件的命名沖突
```javascript
//全局作用域下的發布訂閱模式
(function(){
var Event = (function{
var global = this,
Modal,
_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(const 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] === 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,cache,fn);
},
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 : ,
one: ,
remove: ,
listen:,
trigger:,
var event = this.create();
event.trigger.apply(this,arguments);
}
}();
return Event;
}());
Event.create('namespace1').listen('event');
Event.create('namespace1').trigger('event');
```
## JavaScript 實現發布-訂閱模式的便利性
- 推模型:是指在事件發生時,發布者一次性把所有更改的狀態和數據都推送給訂
閱者。
- 拉模型:發布者僅僅通知訂閱者事件已經發生了,此外發布者要提供一些公開接口共訂閱者來主動拉取數據。
剛好在JavaScript中, arguments 可以很方便地表示參數列表,所以我們一般都會選 擇推模型,使用 Function.prototype.apply 方法把所有參數都推送給訂閱者。