发布-订阅模式

163 阅读6分钟

《JavaScript设计模式与开发实践》读书笔记

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变是,所有依赖于它的对象都将得到通知,在javaScript 开发中,我们一般使用事件模型来替代传统的发布-订阅模式

实现

这里使用顾客预订售楼情况的情景作为例子。

如何一步步实现发布-订阅模式。

  • 指定好谁充当发布者
  • 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册)
  • 左后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)

我们可以往回调函数里填入一些参数,订阅者可以接受这些参数,方便订阅者根据参数做自己的处理。

var salesOffices = {}; // 定义售楼处  
  
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数  
  
salesOffices.listen = function( fn ){ // 增加订阅者  
this.clientList.push( fn ); // 订阅的消息添加进缓存列表  
};  
  
salesOffices.trigger = function(){ // 发布消息  
for( var i = 0, fn; fn = this.clientList[ i++ ]; ){  
fn.apply( this, arguments ); // (2) // arguments 是发布消息时带上的参数  
}  
};  
//下面我们来进行一些简单的测试:  
salesOffices.listen( function( price, squareMeter ){ // 小明订阅消息  
console.log( '价格= ' + price );  
console.log( 'squareMeter= ' + squareMeter );  
});  
  
salesOffices.listen( function( price, squareMeter ){ // 小红订阅消息  
console.log( '价格= ' + price );
console.log( 'squareMeter= ' + squareMeter );  
});  
  
  
salesOffices.trigger( 2000000, 88 ); // 输出:200 万,88 平方米  
salesOffices.trigger( 3000000, 110 ); // 输出:300 万,110 平方米

在上面的实现中,订阅者接收到了发布者的每一个消息,而有的消息是订阅者并不需要的。所以我们需要增加一个key,让订阅者只订阅自己需要的消息。

let salesOffice = {}; // 定义发布者
salesOffice.clientList = {}; // 缓存列表
/**
 * @param key 表示订阅的事件类型,每一个事件类型会对应一个缓存列表,在发生事件时触发事件对应缓存列表中的函数
 * @param fn 表示在订阅的事件发生时触发的函数,它会接受到和事件有关的参数,并执行自己的处理
*/

salesOffice.listen = function ( key , fn ){ // 订阅函数
    if( !this.clientList[key] )
    {
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
}

/**
 * @param key  传入需要触发的事件的key
 * @param [...message] 事件对应的信息
*/

salesOffice.trigger = function (){
    let key = Array.prototype.shift.call(arguments); // 取出消息类型
    let fns = this.clientList[key];
    if( !fns || !fns.length )
    {
        return ;
    }
    
    for( let i = 0 ; i < fns.length ; i++)
    {
        fns[i].apply(this, arguments); // 第二个参数就是事件对应的信息
    }
}

salesOffice.listen("squard88",(arguments) =>{
    console.log("squard88", arguments);
})

  
salesOffice.listen("squard100", (arguments) =>{
    console.log("squard100", arguments);
})

salesOffice.trigger("squard88", 2000);
salesOffice.trigger("squard100",1000);

我们在js中可以把发布-订阅的功能抽离出来,放在一个单独的对象内

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 ];  
  }  
};

取消订阅的事件

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 对象来实现, 订阅者不需要了解消息来自哪个订阅者,发布者也不知道消息会推送给哪些订阅者, Event 作为一个类似“中介者“ 的角色,把订阅者和发布者联系起来。

我们同样可以使用发布-订阅模式来实现模块间的通信,但这里也会引发一些问题。如果模块之间用了太多的全局发布-订阅模式来通信,那么模块和模块之间的联系就被隐藏到了背后。我们最终会搞不清楚消息来自哪个模块,或者消息会流向哪些模块,这会给我们的维护带来麻烦。

先发布再订阅

我们可以试想,现在有一条消息发布,但是没有订阅者接受,那么这条消息就消失了。

在某些情况下,我们需要将这条消息保存下来,等到有对象来订阅它的时候,再重新把消息发布给订阅者。

在实际的开发中, 因为异步的原因,我们不能保证ajax 请求返回的时间,有时候它返回得比较快,而此时用户导航模块的代码还没有加载好(还没有订阅相应事件),特别是在用了一些模块化惰性加载的技术后,这是很可能发生的事情。也许我们还需要一个方案,使得我们的发布—订阅对象拥有先发布后订阅的能力。

我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。

JavaScript 实现发布-订阅模式的便利性

在js中,我们可以使用注册回调函数的形式来代替传统语言(如java)的实现,更优雅和简单。

在js中,arguments 可以很方便的表示参数列表,所以我们一般都会选择推模型,使用 Function.prototype.apply 方法把所有的参数都推送给订阅者。

小结

发布—订阅模式的优点非常明显

  • 一为时间上的解耦
  • 二为对象之间的解耦

它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC 还是MVVM,都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。

当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。

另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个bug 不是件轻松的事情。