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