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

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

  • 在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节点上面绑定过事件函数,那我们就曾经使用过发布—订阅模式,来看看下面这两句简单的代码发生了什么事情:

  1. document.body.addEventListener( 'click', function(){
  2. alert(2);
  3. }, false );
  4. document.body.click(); // 模拟用户点击

在这里需要监控用户点击document.body的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。这很像购房的例子,购房者不知道房子什么时候开售,于是他在订阅消息后等待售楼处发布消息。

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

  1. document.body.addEventListener( 'click', function(){
  2. alert(2);
  3. }, false );
  4. document.body.addEventListener( 'click', function(){
  5. alert(3);
  6. }, false );
  7. document.body.addEventListener( 'click', function(){
  8. alert(4);
  9. }, false );
  10. document.body.click(); // 模拟用户点击

稍微完整一点的例子

  1. // 把发布—订阅的功能提取出来,放在一个单独的对象内:
  2. var event = {
  3. clientList: {}, // 消息列表,一个消息可以有多个订阅者,1对多,比如 {'NBA': ['小明', '小黄', '小张'], 'CBA': ['小红', '小蓝']}
  4. // 订阅功能
  5. listen: function( key, fn ){
  6. if ( !this.clientList[ key ] ){
  7. this.clientList[ key ] = [];
  8. }
  9. this.clientList[ key ].push( fn ); // 把消息和对应的订阅者 添加进 订阅者列表
  10. },
  11. // 发布功能
  12. trigger: function(){
  13. var key = Array.prototype.shift.call( arguments ), // 拿第一个参数(消息,比如 'NBA' )
  14. fns = this.clientList[ key ]; // 订阅者列表
  15. if ( !fns || fns.length === 0 ){ // 如果没人订阅
  16. return false;
  17. }
  18. for( var i = 0, fn; fn = fns[ i++ ]; ){ // 通知订阅者 'NBA已经开播了'
  19. fn.apply( this, arguments ); // arguments 是trigger 时带上的参数
  20. }
  21. }
  22. };
  23. // 再定义一个installEvent函数,这个函数可以给所有的对象都动态安装发布—订阅功能
  24. var installEvent = function( obj ){
  25. for ( var i in event ){
  26. obj[ i ] = event[ i ];
  27. }
  28. };
  29. // 再来测试一番,我们给 电视台对象 动态增加发布—订阅功能:
  30. var cctv5 = {}; // 电视台对象
  31. installEvent( cctv5 ); // 给 电视台对象 安装发布—订阅功能
  32. // 订阅者 小明,小红,小蓝
  33. var ming = function( time ){
  34. console.log( time + 'NBA开播了,我是小明,我订阅了NBA' );
  35. }
  36. var hong = function( time ){
  37. console.log( time + 'NBA开播了,我是小红,我订阅了NBA' );
  38. }
  39. var lan = function( time ){
  40. console.log( time + 'CBA开播了,我是小蓝,我订阅了CBA' );
  41. }
  42. // 订阅消息
  43. cctv5.listen( 'NBA', ming ); // NBA的开播消息,小明订阅了
  44. cctv5.listen( 'NBA', hong ); // NBA的开播消息,小红订阅了
  45. cctv5.listen( 'CBA', lan ); // CBA的开播消息,小蓝订阅了
  46. // 发布消息
  47. cctv5.trigger( 'NBA', '早上10点' ); // 输出:早上10点NBA开播了,我是小明,我订阅了NBA。 早上10点NBA开播了,我是小红,我订阅了NBA
  48. cctv5.trigger( 'CBA', '晚上7点' ); // 输出:晚上7点CBA开播了,我是小C,我订阅了CBA

扩展功能

  1. // 接到上面的代码,在往下新增
  2. // 取消订阅功能
  3. event.remove = function( key, fn ){
  4. var fns = this.clientList[ key ];
  5. if ( !fns ){
  6. return false;
  7. }
  8. if ( !fn ){ // 如果没有传入具体的回调函数(订阅人),表示需要取消key 对应消息的所有订阅者,比如取消掉 订阅'NBA'的所有订阅者
  9. fns && ( fns.length = 0 );
  10. } else {
  11. for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍历订阅的回调函数列表
  12. var _fn = fns[ l ];
  13. if ( _fn === fn ){
  14. fns.splice( l, 1 ); // 删除订阅者的回调函数
  15. }
  16. }
  17. }
  18. };
  19. installEvent( cctv5 ); // 给 电视台对象 重新安装一遍功能
  20. // 取消订阅
  21. cctv5.remove( 'NBA', ming ); // NBA的开播消息,小明取消订阅了
  22. // 发布消息
  23. cctv5.trigger( 'NBA', '早上10点' ); // 输出:早上10点NBA开播了,我是小红,我订阅了NBA

个人整理,有误可留言。
部分参考丛书 《JavaScript设计模式与开发实践》曾探


对另外几种常见设计模式的整理:对几种主要的设计模式的理解 (javascript实现)

  1. 单例模式
  2. 迭代器模式
  3. 代理(中介)模式
  4. 装饰器模式