[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 方法把所有参数都推送给订阅者。