【读书笔记】JavaScript设计模式与开发实践--发布订阅模式

111 阅读4分钟

定义

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

应用

购房者-售楼处,购房者把电话留在售楼处,购房者关注楼盘信息更新,售楼处会依次短信通知。

image.png

作用

  • 时间上解耦(在异步编程中使用发布—订阅模式,无需过多关注对象在异步运行期间的内部状态,而只需要订阅关注的事件节点)
  • 对象间解耦(一个对象不用再显式地调用另外一个对象的某个接口)

DOM 事件

document.body.addEventListener( 'click', function(){ xxx; }, false );
document.body.click(); // 模拟用户点击

订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。

还可以随意增加或者删除订阅者,增加删除任何订阅者都不会影响发布者代码的编写。

document.body.addEventListener( 'click', function(){ xxx2; }, false );
document.body.addEventListener( 'click', function(){ xxx3; }, false ); 
document.body.removeEventListener( 'click', function(){ xxx3; }, false );
document.body.addEventListener( 'click', function(){ xxx4; }, false ); 
document.body.click(); // 模拟用户点击

自定义事件

实现发布-订阅模式的步骤:

  • 指定好发布者
  • 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
  • 发布消息的,发布者遍历缓存列表,依次触发里面存放的订阅者回调函数
var salesOffices = {}; 
// 定义售楼处 
salesOffices.clientList = {};
// 缓存列表,存放订阅者的回调函数 
salesOffices.listen = function( key, fn ){ 
    if ( ! this.clientList[ key ] ){ 
    // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表 
        this.clientList[ key ] = []; 
    } 
    this.clientList[ key ].push( fn ); 
    // 订阅的消息添加进消息缓存列表 
}; 
salesOffices.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 ); 
        // arguments是发布消息时附送的参数
    } 
}; 
salesOffices.listen( 'squareMeter88', function( price ){ 
    // 小明订阅88平方米房子的消息 
    console.log( ’价格= ' + price ); 
    // 输出: 2000000 
}); 
salesOffices.listen( 'squareMeter110', function( price ){
    // 小红订阅110平方米房子的消息 
    console.log( ’价格= ' + price ); 
    // 输出: 3000000 
});
salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布88平方米房子的价格 
salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布110平方米房子的价格

通用实现

提取发布-订阅功能:

var event = { 
    clientList: [],
    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 )// (1); 
        fns = this.clientList[ key ]; 
        if ( ! fns || fns.length === 0 ){ 
        // 如果没有绑定对应的消息 
            return false; 
        } 
        for( var i = 0, fn; fn = fns[ i++ ]; ){ 
            fn.apply( this, arguments ); // (2) // arguments是trigger时带上的参数 
        } 
    } 
};

给对象安装发布-订阅功能:

var installEvent = function( obj ){ 
    for ( var i in event ){ 
        obj[ i ] = event[ i ]; 
    } 
};

测试:

var salesOffices = {}; 
installEvent( salesOffices ); 
salesOffices.listen( 'squareMeter88', function( price ){ 
    // 小明订阅消息 
    console.log( ’价格= ' + price ); 
}); 
salesOffices.listen( 'squareMeter100', function( price ){ 
    // 小红订阅消息 
    console.log( ’价格= ' + price ); 
}); 
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000 
salesOffices.trigger( 'squareMeter100', 3000000 ); // 输出:3000000

取消订阅事件

event.remove = function( key, fn ){ 
    var fns = this.clientList[ 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 ); 
                // 删除订阅者的回调函数 
            } 
        } 
    } 
}; 
var salesOffices = {}; 
var installEvent = function( obj ){ 
    for ( var i in event ){
        obj[ i ] = event[ i ]; 
    } 
} 
installEvent( salesOffices ); 
salesOffices.listen( 'squareMeter88', fn1 = function( price ){ 
    // 小明订阅消息
    console.log( ’价格= ' + price ); 
}); 
salesOffices.listen( 'squareMeter88', fn2 = function( price ){ 
    // 小红订阅消息
    console.log( ’价格= ' + price ); 
}); 
salesOffices.remove( 'squareMeter88', fn1 ); // 删除小明的订阅 
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000

全局Event

上述实现的发布-订阅模式还存在两个问题:

  • 每个发布者对象都添加了listen和trigger方法,以及一个缓存列表clientList,这其实是一种资源浪费。
  • 订阅者跟发布者还是存在一定的耦合性,订阅者至少要知道发布的事件名,才能顺利的订阅到事件。
var Event = (function(){ 
    var clientList = {}, listen, trigger, remove;
    listen = function( key, fn ){ 
    if ( ! clientList[ key ] ){
        clientList[ key ] = []; 
    } 
    clientList[ key ].push( fn ); 
    }; 
    trigger = function(){ 
        var key = Array.prototype.shift.call( arguments ), fns = clientList[ key ]; 
        if ( ! fns || fns.length === 0 ){ 
            return false; 
        } 
        for( var i = 0, fn; fn = fns[ i++ ]; ){ 
            fn.apply( this, arguments );
        } 
    }; 
    remove = function( key, fn ){ 
        var fns = clientList[ key ]; 
        if ( ! fns ){ 
            return false; 
        }
        if ( ! fn ){ 
            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 );
                } 
            } 
        } 
    }; 
    return { 
        listen: listen, 
        trigger: trigger, 
        remove: remove 
    } 
})(); 
Event.listen( 'squareMeter88', function( price ){ 
    // 小红订阅消息 
    console.log( ’价格= ' + price ); // 输出:’价格=2000000' 
}); 
Event.trigger( 'squareMeter88', 2000000 ); // 售楼处发布消息

先发布后订阅

利用一个数组将发布过的消息缓存起来,后续如果有订阅 从数组找到这一条执行,这些缓存的消息执行一次后要从数组删除。

模块间通信

利用Event对象可以在两个模块间通信,这两个模块可以完全不知道对方的存在。

a模块:

function(){
    Event.tigger('add',function(){})
}

b模块:

function(){
    Event.listen('add',function(){})
}

缺点:

使用Event通信太多,会不清楚消息来源及流向,难以维护。项目中如非必要,尽量少使用Event通信。