发布-订阅模式详解(观察者模式)

·  阅读 413
发布-订阅模式详解(观察者模式)

发布-订阅模式(观察者模式)

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

  • 在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实现)

  1. 单例模式
  2. 迭代器模式
  3. 代理(中介)模式
  4. 装饰器模式
分类:
前端
标签: