javascript设计模式与开发实践阅读笔记(8)——观察者模式

时间:2021-03-07 21:43:53

发布-订阅模式,也叫观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

在JavaScript开发中,我们一般用事件模型来替代传统的观察者模式。

书里的现实例子

小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼MM告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼MM决定辞职,因为厌倦了每天回答1000 个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在了售楼处。售楼MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼MM会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。

观察者模式的作用

上面例子中,小明、小红等购买者都是订阅者,他们订阅了房子开售的消息。售楼处作为发布者,会在合适的时候遍历花名册上的电话号码,依次给购房者发布消息。

例子中可以看出这样两点
(1)购房者不用再天天给售楼处打电话咨询开售时间,在合适的时间点,售楼处作为发布者会通知这些消息订阅者。
(2)当有新的购房者出现时,他只需把手机号码留在售楼处,售楼处不关心购房者的任何情况。而售楼处的任何变动也不会影响购买者,只要售楼处记得发短信这件事情。

这表明
(1)观察者模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
(2)说明观察者模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。改变也互不影响,只要之前约定的事件名没有变化,就可以*地改变它们。

我的理解中,观察者模式其实就是一个变相的监听,当发布什么消息后,可以触发添加的监听函数。

具体实现

最简陋的,直接发布信息和触发

 var event = {   // 定义消息的管理者

     clientList : [],    // 缓存列表,存放监听函数

     listen : function( fn ){     // 添加监听函数,存到缓存列表中
this.clientList.push( fn );
}, trigger : function(){ // 消息发布,触发
for( var i = 0, fn; fn = this.clientList[ i++ ]; ){ //遍历缓存列表的监听函数,然后依次调用他们
fn.apply( this, arguments );
}
} }; event.listen( function(){ //当信息发布后,打印 1
console.log(1);
});
event.listen( function( a, b ){ // 当消息发布后,计算值
console.log( a+'和'+b+'的和为'+(a+b) );
}); event.trigger(); //
// undefined和undefined的和为NaN event.trigger( 5,8 ); //
// 5和8的和为13

上面已经简单实现了一个观察者模式,当消息的管理对象发布消息后,依次执行所有监听这个消息的函数。但是如果我们需要监听两种消息,每种消息要能触发相应函数时,我们就只能复制一次event对象,然后给个新名字event2。这样太过繁琐,我们希望event这个消息的管理者本身就可以发布不同的消息。

增加几种消息的类型,添加的监听函数只对相应的消息发布有反应

 var event = {   // 定义消息的管理者

     clientList : {},    // 缓存列表,存放不同的消息下的回调函数

     listen : function( key, fn ){   //key就是消息名
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 );
}
} }; event.listen( "print",function(){ //当对应信息发布后,打印 1
console.log(1);
});
event.listen( "plus",function( a, b ){ // 当对应消息发布后,计算值
console.log( a+'和'+b+'的和为'+(a+b) );
}); event.trigger("print"); // 1
event.trigger( "plus",5,8 ); // 5和8的和为13

现在更进一步,如果我们订阅了消息,但是后面不想订阅了,那就需要取消订阅,移除掉回调函数。我们直接给对象添加相应方法即可。

 event.remove = function( key, fn ){    //消息和对应的回调函数
var fns = this.clientList[ key ]; //找到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 ); // 删除订阅者的回调函数
}
}
}
}; event.listen( "print",fn1=function(){ //当对应信息发布后,打印 2
console.log(2);
}); event.trigger("print"); // event.remove( "print",fn1 );
event.trigger("print"); //什么都没有

所有的信息都可以通过这个全局对象来管理。

模块间通信

比如现在有两个模块,a模块里面有一个按钮,每次点击按钮之后,b模块里的div中会显示按钮的总点击次数,我们用观察者模式来完成,使得a模块和b模块可以在保持封装性的前提下进行通信。

 <!DOCTYPE html>
<html> <body>
<button id="count">点我</button>
<div id="show"></div>
</body> <script type="text/JavaScript">
var a = (function(){
var count = 0;
var button = document.getElementById( 'count' );
button.onclick = function(){
Event.trigger( 'add', count++ );
}
})();
var b = (function(){
var div = document.getElementById( 'show' );
Event.listen( 'add', function( count ){
div.innerHTML = count;
});
})();
</script>
</html>

我们必须要注意一个问题就是,观察者模式不能滥用,模块之间如果用了太多的观察者模式来通信,那么模块与模块之间的联系就被隐藏到了背后,这会给我们的维护带来一些麻烦。

解决最后的问题

前面的代码都是先订阅,再发布,比如这样

event.listen( "print",fn1=function(){
console.log(2);
}); event.trigger("print"); // 如果我们把他们反过来呢,如果我们先发布了呢 event.trigger("print"); // 什么都不会发生 event.listen( "print",fn1=function(){
console.log(2);
});

因为很多懒加载技术存在,有的时候可能需要先把发布的信息保留下来,当订阅时,触发相应的回调函数。
而且作为全局的对象,大家都通过它发布消息和订阅消息,最后难免会出现重名的情况,所以,event对象最好也能拥有创建命名空间的能力。

终极代码如下:

 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; var 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;
}; _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; //返回观察者对象
})();

总结

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