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

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

总结

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

javascript设计模式与开发实践阅读笔记(8)——观察者模式的更多相关文章

  1. javascript设计模式与开发实践阅读笔记(4)——单例模式

    定义 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点. 具体来说,就是保证有些对象有且只有一个,比如线程池.全局缓存.浏览器中的window 对象等.在js中单例模式用途很广,比如登录 ...

  2. javascript设计模式与开发实践阅读笔记(7)——迭代器模式

    迭代器模式:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示. 迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺 ...

  3. javascript设计模式与开发实践阅读笔记(6)——代理模式

    代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问. 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对 ...

  4. javascript设计模式与开发实践阅读笔记(5)——策略模式

    策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换. 我的理解就是把各种方法封装成函数,同时存在一个可以调用这些方法的公共函数.这样做的好处是可以消化掉内部的分支判断,使代码效率 ...

  5. javascript设计模式与开发实践阅读笔记(9)——命令模式

    命令模式:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系. 说法很复 ...

  6. javascript设计模式与开发实践阅读笔记(11)—— 模板方法模式

    模板方法模式: 由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类.通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序.子类通过继承这个抽象类,也继 ...

  7. JavaScript设计模式与开发实践——读书笔记1.高阶函数(上)

    说来惭愧,4个多月未更新了.4月份以后就开始忙起来了,论文.毕设.毕业旅行等七七八八的事情占据了很多时间,毕业之后开始忙碌的工作,这期间一直想写博客,但是一直没能静下心写.这段时间在看<Java ...

  8. 《JavaScript设计模式与开发实践》笔记第八章 发布-订阅模式

    第八章 发布-订阅模式 发布-订阅模式描述 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布-订阅模式可以广泛应用于 ...

  9. JavaScript设计模式与开发实践——读书笔记1.高阶函数(下)

    上部分主要介绍高阶函数的常见形式,本部分将着重介绍高阶函数的高级应用. 1.currying currying指的是函数柯里化,又称部分求值.一个currying的函数会先接受一些参数,但不立即求值, ...

随机推荐

  1. UILabel的常用属性

    UILabel常用属性1——实例化和设置文字 // 实例化UILabel并指定其边框 UILabel *label = [[UILabel alloc]initWithFrame:CGRectMake ...

  2. 6.HotSpot垃圾收集器

    HotSpot JVM收集器 上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器.如果两个收集器之间存在连线,就说明它们可以搭配使用. 并发和并行 先解释下什么是垃圾收集器的上下文语境 ...

  3. andorid 练习之黑名单

    activity_mian.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ...

  4. Android Preference使用

    Android Preference经常使用在例如设置的功能,Android提供preference这个键值对的方式来处理这种情况,自动保存这些数据,并立时生效,这种就是使用android share ...

  5. iOS改变NavigationBar的返回键和标题颜色、大小

    UIButton *backBtn = [UIButton buttonWithType:UIButtonTypeCustom]; [UIPubic initHeadViewBackImgWithBt ...

  6. Comet:基于 HTTP 长连接的“服务器推”技术

    “服务器推”技术的应用 请访问 Ajax 技术资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档.教程.论坛.blog.wiki 和新闻.任何 Ajax 的新信息都能在这里找到. c ...

  7. Frogger

    Freddy Frog is sitting on a stone in the middle of a lake. Suddenly he notices Fiona Frog who is sit ...

  8. DataTable与List互换

    public static class List2DataTable { #region "Convert Generic List to DataTable" /// <s ...

  9. 去除html标签 正则表达式

    /// <summary>        /// 去除html标签        /// </summary>        public static string Clea ...

  10. Android Studio安装genymotion模拟器

    1.Genymotion的安装: Genymotion无疑是目前最快最好用的模拟器.官网下载地址:https://www.genymotion.com/ 先注册,然后下载,安装VirtualBox最简 ...