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

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设计模式和开发实践》一书

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容