《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 })();

 

总结

观察者模式的特点就是可以响应特定的信息,完成相应的操作。

 

posted @ 2016-12-06 18:05  出世Sunny  阅读(413)  评论(0编辑  收藏  举报