0%

JavaScript设计模式--发布订阅模式

异步是浏览器编程中非常常见的,比如在发起一个网络请求后,我们需要等待请求的结果并据此作出反应,然而,没有人准确知道请求什么时候能够完成,要解决这个问题,就只能以异步的方式来等待请求完成后调用特定的方法。通常我们以回调函数的形式来完成这个过程,但是回调函数有着相当的局限性。

如何实现发布订阅模式

要实现一个基本的发布订阅模式,可以分为以下几步:

1 指点好发布者
2 为发布者配置缓存列表,用于存放回调函数
3 发布消息时将缓存列表中的函数一次触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 ), // (1);
fns = this.clientList[ key ];
if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
return false;
}
for( var i = 0, fn; fn = fns[ i++ ]; ){
fn.apply( this, arguments ); // (2) // arguments 是 trigger 时带上的参数
}
},
remove:function(key,fn){
var fns=this.clientList[key];
if (!fns){ // 如果 key 对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn){// 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
fns&&(fns.length=0);
}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 ){
obj[ i ] = event[ i ];
}
};

实例:登陆成功事件

假设在使用ajax登陆成功后,需要完成一系列的任务比如设置头像,更新个人信息,刷新购物车列表等等,将这些事件统统放到登陆的回调函数里面会使得代码臃肿,而且不符合单一职责法则。
这时,可以使用login作为一个发布对象,各个功能模块向login订阅登陆成功的事件,一旦登陆成功,登录模块只需要将登陆成功的消息trigger,则向其订阅过的模块都可以在接收到消息时做出各自应有的反应。

异步

上面的方案其实还存在着潜在的风险,即各个模块还未来得及向登录模块订阅,登陆已经成功了,在发送消息过后发生的订阅将无法正确发布相应的消息。
因此,我们需要更多的缓存,用来存储已经发布但是还未被订阅的消息。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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;
};//使用迭代器模式定义each方法
_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(){
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, 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;
})();

上面的代码主要核心在于_create方法返回的listen和trigger方法,他们共享一个offlineStack,这个数组的用途就是存储发生在订阅之前的消息,一旦出现了订阅行为,则所有被提前缓存的事件被触发,之后缓存空间被释放,除非重新执行create方法,不然之后所有的事件都需要先订阅再发布,也就是说,只有在第一次订阅发生之前的事件会被缓存,之后的行为还是按照先订阅再发放的顺序,因此,上面的代码其实还有优化的余地,比如凡是没有被订阅的消息都会被缓存起来等。