《javascript设计模式与开发实践》阅读笔记(8)—— 观察者模式
发布-订阅模式,也叫观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
在JavaScript开发中,我们一般用事件模型来替代传统的观察者模式。
书里的现实例子
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼MM告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼MM决定辞职,因为厌倦了每天回答1000 个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在了售楼处。售楼MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼MM会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。
观察者模式的作用
上面例子中,小明、小红等购买者都是订阅者,他们订阅了房子开售的消息。售楼处作为发布者,会在合适的时候遍历花名册上的电话号码,依次给购房者发布消息。
例子中可以看出这样两点
(1)购房者不用再天天给售楼处打电话咨询开售时间,在合适的时间点,售楼处作为发布者会通知这些消息订阅者。
(2)当有新的购房者出现时,他只需把手机号码留在售楼处,售楼处不关心购房者的任何情况。而售楼处的任何变动也不会影响购买者,只要售楼处记得发短信这件事情。
这表明
(1)观察者模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
(2)说明观察者模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。改变也互不影响,只要之前约定的事件名没有变化,就可以自由地改变它们。
我的理解中,观察者模式其实就是一个变相的监听,当发布什么消息后,可以触发添加的监听函数。
具体实现
最简陋的,直接发布信息和触发
1 var event = { // 定义消息的管理者
2
3 clientList : [], // 缓存列表,存放监听函数
4
5 listen : function( fn ){ // 添加监听函数,存到缓存列表中
6 this.clientList.push( fn );
7 },
8
9 trigger : function(){ // 消息发布,触发
10 for( var i = 0, fn; fn = this.clientList[ i++ ]; ){ //遍历缓存列表的监听函数,然后依次调用他们
11 fn.apply( this, arguments );
12 }
13 }
14
15 };
16
17 event.listen( function(){ //当信息发布后,打印 1
18 console.log(1);
19 });
20 event.listen( function( a, b ){ // 当消息发布后,计算值
21 console.log( a+'和'+b+'的和为'+(a+b) );
22 });
23
24 event.trigger(); // 1
25 // undefined和undefined的和为NaN
26
27 event.trigger( 5,8 ); // 1
28 // 5和8的和为13
上面已经简单实现了一个观察者模式,当消息的管理对象发布消息后,依次执行所有监听这个消息的函数。但是如果我们需要监听两种消息,每种消息要能触发相应函数时,我们就只能复制一次event对象,然后给个新名字event2。这样太过繁琐,我们希望event这个消息的管理者本身就可以发布不同的消息。
增加几种消息的类型,添加的监听函数只对相应的消息发布有反应
1 var event = { // 定义消息的管理者
2
3 clientList : {}, // 缓存列表,存放不同的消息下的回调函数
4
5 listen : function( key, fn ){ //key就是消息名
6 if ( !this.clientList[ key ] ){ //如果列表中没有对应消息
7 this.clientList[ key ] = []; //新建该消息的回调函数数组
8 }
9 this.clientList[ key ].push( fn ); // 把监听函数添加到相应数组中
10 },
11
12 trigger : function(){ // 发布消息
13 var key = Array.prototype.shift.call( arguments ), // 取出消息类型
14 fns = this.clientList[ key ]; // 取出该消息对应的监听函数集合
15 if ( !fns || fns.length === 0 ){ // 如果没有人订阅该消息,则返回
16 return false;
17 }
18 for( var i = 0, fn; fn = fns[ i++ ]; ){ //遍历回调函数,依次执行
19 fn.apply( this, arguments );
20 }
21 }
22
23 };
24
25 event.listen( "print",function(){ //当对应信息发布后,打印 1
26 console.log(1);
27 });
28 event.listen( "plus",function( a, b ){ // 当对应消息发布后,计算值
29 console.log( a+'和'+b+'的和为'+(a+b) );
30 });
31
32 event.trigger("print"); // 1
33 event.trigger( "plus",5,8 ); // 5和8的和为13
现在更进一步,如果我们订阅了消息,但是后面不想订阅了,那就需要取消订阅,移除掉回调函数。我们直接给对象添加相应方法即可。
1 event.remove = function( key, fn ){ //消息和对应的回调函数
2 var fns = this.clientList[ key ]; //找到key对应的函数数组
3 if ( !fns ){ //如果key 对应的消息没有被人订阅,则直接返回
4 return false;
5 }
6 if ( !fn ){ // 如果没有传入具体的回调函数,表示需要取消key 对应消息的所有订阅
7 fns && ( fns.length = 0 ); //如果数组存在就把数组清空
8 }else{
9 for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍历订阅的回调函数列表,这里只是经验主义,反向找效率高一点
10 var _fn = fns[ l ]; //保留遍历的函数引用
11 if ( _fn === fn ){ //找到了要删除的函数
12 fns.splice( l, 1 ); // 删除订阅者的回调函数
13 }
14 }
15 }
16 };
17
18 event.listen( "print",fn1=function(){ //当对应信息发布后,打印 2
19 console.log(2);
20 });
21
22 event.trigger("print"); // 2
23
24 event.remove( "print",fn1 );
25 event.trigger("print"); //什么都没有
所有的信息都可以通过这个全局对象来管理。
模块间通信
比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的div中会显示按钮的总点击次数,我们用观察者模式来完成,使得a模块和b模块可以在保持封装性的前提下进行通信。
1 <!DOCTYPE html>
2 <html>
3
4 <body>
5 <button id="count">点我</button>
6 <div id="show"></div>
7 </body>
8
9 <script type="text/JavaScript">
10 var a = (function(){
11 var count = 0;
12 var button = document.getElementById( 'count' );
13 button.onclick = function(){
14 Event.trigger( 'add', count++ );
15 }
16 })();
17 var b = (function(){
18 var div = document.getElementById( 'show' );
19 Event.listen( 'add', function( count ){
20 div.innerHTML = count;
21 });
22 })();
23 </script>
24 </html>
我们必须要注意一个问题就是,观察者模式不能滥用,模块之间如果用了太多的观察者模式来通信,那么模块与模块之间的联系就被隐藏到了背后,这会给我们的维护带来一些麻烦。
解决最后的问题
前面的代码都是先订阅,再发布,比如这样
event.listen( "print",fn1=function(){
console.log(2);
});
event.trigger("print"); // 2
如果我们把他们反过来呢,如果我们先发布了呢
event.trigger("print"); // 什么都不会发生
event.listen( "print",fn1=function(){
console.log(2);
});
因为很多懒加载技术存在,有的时候可能需要先把发布的信息保留下来,当订阅时,触发相应的回调函数。
而且作为全局的对象,大家都通过它发布消息和订阅消息,最后难免会出现重名的情况,所以,event对象最好也能拥有创建命名空间的能力。
终极代码如下:
1 var Event = (function(){
2 var global = this,
3 Event, //真正起作用的对象
4 _default = 'default'; //标识符
5
6 Event = (function(){
7 var _listen,
8 _trigger,
9 _remove,
10 _slice = Array.prototype.slice,
11 _shift = Array.prototype.shift,
12 _unshift = Array.prototype.unshift,
13 namespaceCache = {}, //命名空间缓存
14 _create,
15 find;
16
17 var each = function( ary, fn ){ //遍历执行方法
18 var ret;
19 for ( var i = 0, l = ary.length; i < l; i++ ){
20 var n = ary[i];
21 ret = fn.call( n, i, n);
22 }
23 return ret;
24 };
25
26 _listen = function( key, fn, cache ){ //注册触发信息和函数
27 if ( !cache[ key ] ){
28 cache[ key ] = [];
29 }
30 cache[key].push( fn );
31 };
32
33 _remove = function( key, cache ,fn){ //移除函数和触发信息
34 if ( cache[ key ] ){
35 if( fn ){
36 for( var i = cache[ key ].length; i >= 0; i-- ){
37 if( cache[ key ][i] === fn ){
38 cache[ key ].splice( i, 1 );
39 }
40 }
41 }else{
42 cache[ key ] = [];
43 }
44 }
45 };
46
47 _trigger = function(){ //发布触发信息,执行函数
48 var cache = _shift.call(arguments),
49 key = _shift.call(arguments),
50 args = arguments,
51 _self = this,
52 ret,
53 stack = cache[ key ];
54
55 if ( !stack || !stack.length ){
56 return;
57 }
58 return each( stack, function(){
59 return this.apply( _self, args );
60 });
61 };
62
63 _create = function( namespace ){ //创建命名空间
64 var namespace = namespace || _default;
65 var cache = {},
66 offlineStack = [], // 离线事件
67 ret = {
68 listen: function( key, fn, last ){
69 _listen( key, fn, cache );
70 if ( offlineStack === null ){
71 return;
72 }
73 if ( last === 'last' ){
74 offlineStack.length && offlineStack.pop()();
75 }else{
76 each( offlineStack, function(){
77 this();
78 });
79 }
80 offlineStack = null;
81 },
82 one: function( key, fn, last ){
83 _remove( key, cache );
84 this.listen( key, fn ,last );
85 },
86 remove: function( key, fn ){
87 _remove( key, cache ,fn);
88 },
89 trigger: function(){
90 var fn,
91 args,
92 _self = this;
93
94 _unshift.call( arguments, cache );
95 args = arguments;
96 fn = function(){
97 return _trigger.apply( _self, args );
98 };
99 if ( offlineStack ){
100 return offlineStack.push( fn );
101 }
102 return fn();
103 }
104 };
105 return namespace ?
106 ( namespaceCache[ namespace ] ? namespaceCache[ namespace ] : namespaceCache[ namespace ] = ret )
107 : ret;
108 };
109
110 return { //实际的观察者对象
111 create: _create, //传入创建命名空间的字面量
112 one: function( key,fn, last ){
113 var event = this.create( );
114 event.one( key,fn,last );
115 },
116 remove: function( key,fn ){
117 var event = this.create( );
118 event.remove( key,fn );
119 },
120 listen: function( key, fn, last ){
121 var event = this.create( );
122 event.listen( key, fn, last );
123 },
124 trigger: function(){
125 var event = this.create( );
126 event.trigger.apply( this, arguments );
127 }
128 };
129 })();
130
131 return Event; //返回观察者对象
132 })();
总结
观察者模式的特点就是可以响应特定的信息,完成相应的操作。