JavaScript 设计模式(中)——5.发布订阅模式(观察者

2019-12-22  本文已影响0人  Haleng

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

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

5.1 发布- 订阅模式的作用

发布—订阅模式的应用都非常之广泛,首先看一个现实中的例子,小明最近在看房子,到了某个售楼处之后才被告知,该楼盘的房子早已售罄,小明离开之前,把电话号码留在了售楼处。售楼处答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们;

在这个例子中使用发布—订阅模式有着显而易见的优点:

  1. 购房者不用再天天给售楼处打电话咨询开售时间,在合适的时间点,售楼处作为发布者会通知这些消息订阅者。这点说明发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案;
  2. 购房者和售楼处之间不再强耦合在一起,当有新的购房者出现时,他只需把手机号码留在售楼处,售楼处不关心购房者的任何情况,同时售楼处的任何变动也不会影响购买者,只要售楼处记得发短信这件事情。这点说明发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。

5.2 发布订阅模式的实现

  1. 实现上述发布订阅模式实例步骤:
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数
salesOffices.listen = function( fn ){ // 增加订阅者
  this.clientList.push( fn ); // 订阅的消息添加进缓存列表
};
salesOffices.trigger = function(){ // 发布消息
  for( var i = 0, fn; fn = this.clientList[ i++ ]; ){
  f  n.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 平方米
  1. 发布订阅模式的通用实现
// 第一步:把发布订阅的功能提取出来,放在一个单独的对象内
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 时带上的参数
    }
  }
};
// 第二步:定义 installEvent 函数给所有的对象都动态安装发布—订阅功能
var installEvent = function( obj ){
  for ( var i in event ){
    obj[ i ] = event[ i ];
  }
};
// 测试:给售楼处对象 salesOffices 动态增加发布—订阅功能
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

5.3 取消订阅的事件

有时也许需要取消订阅事件的功能,因此给 event 对象增加 remove 方法,如下:

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

5.4 网站登录实例(P116)

5.5 全局的发布订阅对象

刚刚实现的发布订阅模式,给售楼处对象和登录对象都添加了订阅和发布的功能,这里还存在两个小问题:

  1. 给每个发布者对象都添加了 listentrigger 方法,以及一个缓存列表 clientList ,这其实是一种资源浪费;
  2. 订阅者跟售楼处对象还是存在一定的耦合性,订阅者至少要知道售楼处对象的名字是 salesOffices ,才能顺利的订阅到事件;

实际上,订阅者没必要亲自去售楼处,只需要把订阅的请求交给中介公司,而各大房产公司也只需要通过中介公司来发布房子信息。为了保证订阅者和发布者能顺利通信,订阅者和发布者都必须知道这个中介公司。因此,发布—订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者, Event 作为一个类似“中介者”的角色,把订阅者和发布者联系起来。见如下代码:

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 ); // 售楼处发布消息

5.6 模块间通信

我们利用上一节中实现的发布订阅模式的全局 Event 对象可以在两个封装良好的模块中进行通信,而这两个模块可以完全不知道对方的存在。比如现在有两个模块, 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>

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

在 JavaScript 中,无需去选择使用推模型还是拉模型。推模型是指在事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者。拉模型不同的地方是,发布者仅仅通知订阅者事件已经发生了,此外发布者要提供一些公开的接口供订阅者来主动拉取数据。拉模型的好处是可以让订阅者“按需获取”,但同时有可能让发布者变成一个“门户大开”的对象,同时增加了代码量和复杂度。刚好在 JavaScript 中, arguments 可以很方便地表示参数列表,所以我们一般都会选择推模型,使用 Function.prototype.apply 方法把所有参数都推送给订阅者。

5.8 发布订阅模式小结

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

发布订阅模式的优点一为时间上的解耦,二为对象之间的解耦。发布订阅模式缺点就是创建订阅者本身要消耗一定的时间和内存,若订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中;若过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。

系列链接

  1. JavaScript 设计模式(上)——基础知识
  2. JavaScript 设计模式(中)——1.单例模式
  3. JavaScript 设计模式(中)——2.策略模式
  4. JavaScript 设计模式(中)——3.代理模式
  5. JavaScript 设计模式(中)——4.迭代器模式
  6. JavaScript 设计模式(中)——5.发布订阅模式
  7. JavaScript 设计模式(中)——6.命令模式
  8. JavaScript 设计模式(中)——7.组合模式
  9. JavaScript 设计模式(中)——8.模板方法模式
  10. JavaScript 设计模式(中)——9.享元模式
  11. JavaScript 设计模式(中)——10.职责链模式
  12. JavaScript 设计模式(中)——11. 中介者模式
  13. JavaScript 设计模式(中)——12. 装饰者模式
  14. JavaScript 设计模式(中)——13.状态模式
  15. JavaScript 设计模式(中)——14.适配器模式
  16. JavaScript 设计模式(下)——设计原则
  17. JavaScript 设计模式练习代码

本文主要参考了《JavaScript设计模式和开发实践》一书

上一篇 下一篇

猜你喜欢

热点阅读