发布-订阅模式(观察者模式)
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
- 在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
现实生活的例子
小明了解到cctv5即将播放NBA的比赛
于是小明记下了cctv5的电话,以后每天都会打电话过去询问NBA什么时候开播。除了小明,还有小红、小强、小龙也会每天向cctv5咨询这个问题。一个星期过后,cctv5的MM决定辞职,因为厌倦了每天回答1000个相同内容的电话。
当然现实中cctv5没有这么笨,实际上故事是这样的:小明离开之前,把电话号码发送短信给了cctv5的工作人员。工作人员MM答应他,NBA一开播就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在订阅者名册上,NBA开播的时候,工作人员MM会翻开订阅者名册,遍历上面的电话号码,依次发送一条短信来通知他们。
在刚刚的例子中,发送短信通知就是一个典型的发布—订阅模式,小明、小红等订阅者都是订阅者,他们订阅了NBA开播的消息。cctv5作为发布者,会在合适的时候遍历订阅者名册上的电话号码,依次给订阅者发布消息。
- 可以发现,在这个例子中使用发布—订阅模式有着显而易见的优点。
- 订阅者不用再天天给cctv5打电话咨询开售时间,在合适的时间点,cctv5作为发布者会通知这些消息给订阅者。
- 订阅者和cctv5之间不再强耦合在一起,当有新的订阅者出现时,他只需把手机号码留在cctv5,cctv5不关心订阅者的任何情况,不管订阅者是男是女还是一只猴子。而cctv5的任何变动也不会影响订阅者,比如工作人员MM离职,cctv5从北京搬到深圳,这些改变都跟订阅者无关,只要cctv5记得发短信这件事情。
代码中举例:
实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就曾经使用过发布—订阅模式,来看看下面这两句简单的代码发生了什么事情:
document.body.addEventListener( 'click', function(){
alert(2);
}, false );
document.body.click(); // 模拟用户点击
在这里需要监控用户点击document.body的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。这很像购房的例子,购房者不知道房子什么时候开售,于是他在订阅消息后等待售楼处发布消息。
当然我们还可以随意增加或者删除订阅者,增加任何订阅者都不会影响发布者代码的编写:
document.body.addEventListener( 'click', function(){
alert(2);
}, false );
document.body.addEventListener( 'click', function(){
alert(3);
}, false );
document.body.addEventListener( 'click', function(){
alert(4);
}, false );
document.body.click(); // 模拟用户点击
稍微完整一点的例子
// 把发布—订阅的功能提取出来,放在一个单独的对象内:
var event = {
clientList: {}, // 消息列表,一个消息可以有多个订阅者,1对多,比如 {'NBA': ['小明', '小黄', '小张'], 'CBA': ['小红', '小蓝']}
// 订阅功能
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 ), // 拿第一个参数(消息,比如 'NBA' )
fns = this.clientList[ key ]; // 订阅者列表
if ( !fns || fns.length === 0 ){ // 如果没人订阅
return false;
}
for( var i = 0, fn; fn = fns[ i++ ]; ){ // 通知订阅者 'NBA已经开播了'
fn.apply( this, arguments ); // arguments 是trigger 时带上的参数
}
}
};
// 再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布—订阅功能
var installEvent = function( obj ){
for ( var i in event ){
obj[ i ] = event[ i ];
}
};
// 再来测试一番,我们给 电视台对象 动态增加发布—订阅功能:
var cctv5 = {}; // 电视台对象
installEvent( cctv5 ); // 给 电视台对象 安装发布—订阅功能
// 订阅者 小明,小红,小蓝
var ming = function( time ){
console.log( time + 'NBA开播了,我是小明,我订阅了NBA' );
}
var hong = function( time ){
console.log( time + 'NBA开播了,我是小红,我订阅了NBA' );
}
var lan = function( time ){
console.log( time + 'CBA开播了,我是小蓝,我订阅了CBA' );
}
// 订阅消息
cctv5.listen( 'NBA', ming ); // NBA的开播消息,小明订阅了
cctv5.listen( 'NBA', hong ); // NBA的开播消息,小红订阅了
cctv5.listen( 'CBA', lan ); // CBA的开播消息,小蓝订阅了
// 发布消息
cctv5.trigger( 'NBA', '早上10点' ); // 输出:早上10点NBA开播了,我是小明,我订阅了NBA。 早上10点NBA开播了,我是小红,我订阅了NBA
cctv5.trigger( 'CBA', '晚上7点' ); // 输出:晚上7点CBA开播了,我是小C,我订阅了CBA
扩展功能
// 接到上面的代码,在往下新增
// 取消订阅功能
event.remove = function( key, fn ){
var fns = this.clientList[ key ];
if ( !fns ){
return false;
}
if ( !fn ){ // 如果没有传入具体的回调函数(订阅人),表示需要取消key 对应消息的所有订阅者,比如取消掉 订阅'NBA'的所有订阅者
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( cctv5 ); // 给 电视台对象 重新安装一遍功能
// 取消订阅
cctv5.remove( 'NBA', ming ); // NBA的开播消息,小明取消订阅了
// 发布消息
cctv5.trigger( 'NBA', '早上10点' ); // 输出:早上10点NBA开播了,我是小红,我订阅了NBA
个人整理,有误可留言。
部分参考丛书 《JavaScript设计模式与开发实践》曾探
对另外几种常见设计模式的整理:对几种主要的设计模式的理解 (javascript实现)
- 单例模式
- 迭代器模式
- 代理(中介)模式
- 装饰器模式