加载中...

(32)设计模式之观察者模式


介绍

观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

使用观察者模式的好处:

  1. 支持简单的广播通信,自动通知所有已经订阅过的对象。
  2. 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  3. 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

正文(版本一)

JS里对观察者模式的实现是通过回调来实现的,我们来先定义一个pubsub对象,其内部包含了3个方法:订阅、退订、发布。

  1. var pubsub = {};
  2. (function (q) {
  3.  
  4.     var topics = {}, // 回调函数存放的数组
  5.         subUid = -1;
  6.     // 发布方法
  7.     q.publish = function (topic, args) {
  8.  
  9.         if (!topics[topic]) {
  10.             return false;
  11.         }
  12.  
  13.         setTimeout(function () {
  14.             var subscribers = topics[topic],
  15.                 len = subscribers ? subscribers.length : 0;
  16.  
  17.             while (len--) {
  18.                 subscribers[len].func(topic, args);
  19.             }
  20.         }, 0);
  21.  
  22.         return true;
  23.  
  24.     };
  25.     //订阅方法
  26.     q.subscribe = function (topic, func) {
  27.  
  28.         if (!topics[topic]) {
  29.             topics[topic] = [];
  30.         }
  31.  
  32.         var token = (++subUid).toString();
  33.         topics[topic].push({
  34.             token: token,
  35.             func: func
  36.         });
  37.         return token;
  38.     };
  39.     //退订方法
  40.     q.unsubscribe = function (token) {
  41.         for (var m in topics) {
  42.             if (topics[m]) {
  43.                 for (var i = 0, j = topics[m].length; i < j; i++) {
  44.                     if (topics[m][i].token === token) {
  45.                         topics[m].splice(i, 1);
  46.                         return token;
  47.                     }
  48.                 }
  49.             }
  50.         }
  51.         return false;
  52.     };
  53. } (pubsub));

使用方式如下:

  1. //来,订阅一个
  2. pubsub.subscribe('example1', function (topics, data) {
  3.     console.log(topics + ": " + data);
  4. });
  5.  
  6. //发布通知
  7. pubsub.publish('example1', 'hello world!');
  8. pubsub.publish('example1', ['test', 'a', 'b', 'c']);
  9. pubsub.publish('example1', [{ 'color': 'blue' }, { 'text': 'hello'}]);

怎么样?用起来是不是很爽?但是这种方式有个问题,就是没办法退订订阅,要退订的话必须指定退订的名称,所以我们再来一个版本:

  1. //将订阅赋值给一个变量,以便退订
  2. var testSubscription = pubsub.subscribe('example1', function (topics, data) {
  3.     console.log(topics + ": " + data);
  4. });
  5.  
  6. //发布通知
  7. pubsub.publish('example1', 'hello world!');
  8. pubsub.publish('example1', ['test', 'a', 'b', 'c']);
  9. pubsub.publish('example1', [{ 'color': 'blue' }, { 'text': 'hello'}]);
  10.  
  11. //退订
  12. setTimeout(function () {
  13.     pubsub.unsubscribe(testSubscription);
  14. }, 0);
  15.  
  16. //再发布一次,验证一下是否还能够输出信息
  17. pubsub.publish('example1', 'hello again! (this will fail)');

版本二

我们也可以利用原型的特性实现一个观察者模式,代码如下:

  1. function Observer() {
  2.     this.fns = [];
  3. }
  4. Observer.prototype = {
  5.     subscribe: function (fn) {
  6.         this.fns.push(fn);
  7.     },
  8.     unsubscribe: function (fn) {
  9.         this.fns = this.fns.filter(
  10.                         function (el) {
  11.                             if (el !== fn) {
  12.                                 return el;
  13.                             }
  14.                         }
  15.                     );
  16.     },
  17.     update: function (o, thisObj) {
  18.         var scope = thisObj || window;
  19.         this.fns.forEach(
  20.                         function (el) {
  21.                             el.call(scope, o);
  22.                         }
  23.                     );
  24.     }
  25. };
  26.  
  27. //测试
  28. var o = new Observer;
  29. var f1 = function (data) {
  30.     console.log('Robbin: ' + data + ', 赶紧干活了!');
  31. };
  32.  
  33. var f2 = function (data) {
  34.     console.log('Randall: ' + data + ', 找他加点工资去!');
  35. };
  36.  
  37. o.subscribe(f1);
  38. o.subscribe(f2);
  39.  
  40. o.update("Tom回来了!")
  41.  
  42. //退订f1
  43. o.unsubscribe(f1);
  44. //再来验证
  45. o.update("Tom回来了!");

如果提示找不到filter或者forEach函数,可能是因为你的浏览器还不够新,暂时不支持新标准的函数,你可以使用如下方式自己定义:

  1. if (!Array.prototype.forEach) {
  2.     Array.prototype.forEach = function (fn, thisObj) {
  3.         var scope = thisObj || window;
  4.         for (var i = 0, j = this.length; i < j; ++i) {
  5.             fn.call(scope, this[i], i, this);
  6.         }
  7.     };
  8. }
  9. if (!Array.prototype.filter) {
  10.     Array.prototype.filter = function (fn, thisObj) {
  11.         var scope = thisObj || window;
  12.         var a = [];
  13.         for (var i = 0, j = this.length; i < j; ++i) {
  14.             if (!fn.call(scope, this[i], i, this)) {
  15.                 continue;
  16.             }
  17.             a.push(this[i]);
  18.         }
  19.         return a;
  20.     };
  21. }

版本三

如果想让多个对象都具有观察者发布订阅的功能,我们可以定义一个通用的函数,然后将该函数的功能应用到需要观察者功能的对象上,代码如下:

  1. //通用代码
  2. var observer = {
  3.     //订阅
  4.     addSubscriber: function (callback) {
  5.         this.subscribers[this.subscribers.length] = callback;
  6.     },
  7.     //退订
  8.     removeSubscriber: function (callback) {
  9.         for (var i = 0; i this.subscribers.length; i++) {
  10.             if (this.subscribers[i] === callback) {
  11.                 delete (this.subscribers[i]);
  12.             }
  13.         }
  14.     },
  15.     //发布
  16.     publish: function (what) {
  17.         for (var i = 0; i this.subscribers.length; i++) {
  18.             if (typeof this.subscribers[i] === 'function') {
  19.                 this.subscribers[i](what);
  20.             }
  21.         }
  22.     },
  23.     // 将对象o具有观察者功能
  24.     make: function (o) { 
  25.         for (var i in this) {
  26.             o[i] = this[i];
  27.             o.subscribers = [];
  28.         }
  29.     }
  30. };

然后订阅2个对象blogger和user,使用observer.make方法将这2个对象具有观察者功能,代码如下:

  1. var blogger = {
  2.     recommend: function (id) {
  3.         var msg = 'dudu 推荐了的帖子:' + id;
  4.         this.publish(msg);
  5.     }
  6. };
  7.  
  8. var user = {
  9.     vote: function (id) {
  10.         var msg = '有人投票了!ID=' + id;
  11.         this.publish(msg);
  12.     }
  13. };
  14.  
  15. observer.make(blogger);
  16. observer.make(user);

使用方法就比较简单了,订阅不同的回调函数,以便可以注册到不同的观察者对象里(也可以同时注册到多个观察者对象里):

  1. var tom = {
  2.     read: function (what) {
  3.         console.log('Tom看到了如下信息:' + what)
  4.     }
  5. };
  6.  
  7. var mm = {
  8.     show: function (what) {
  9.         console.log('mm看到了如下信息:' + what)
  10.     }
  11. };
  12. // 订阅
  13. blogger.addSubscriber(tom.read);
  14. blogger.addSubscriber(mm.show);
  15. blogger.recommend(123); //调用发布
  16.  
  17. //退订
  18. blogger.removeSubscriber(mm.show);
  19. blogger.recommend(456); //调用发布
  20.  
  21. //另外一个对象的订阅
  22. user.addSubscriber(mm.show);
  23. user.vote(789); //调用发布

jQuery版本

根据jQuery1.7版新增的on/off功能,我们也可以定义jQuery版的观察者:

  1. (function ($) {
  2.  
  3.     var o = $({});
  4.  
  5.     $.subscribe = function () {
  6.         o.on.apply(o, arguments);
  7.     };
  8.  
  9.     $.unsubscribe = function () {
  10.         o.off.apply(o, arguments);
  11.     };
  12.  
  13.     $.publish = function () {
  14.         o.trigger.apply(o, arguments);
  15.     };
  16.  
  17. } (jQuery));

调用方法比上面3个版本都简单:

  1. //回调函数
  2. function handle(e, a, b, c) {
  3.     // `e`是事件对象,不需要关注
  4.     console.log(+ b + c);
  5. };
  6.  
  7. //订阅
  8. $.subscribe("/some/topic", handle);
  9. //发布
  10. $.publish("/some/topic", ["a", "b", "c"]); // 输出abc
  11.  
  12. $.unsubscribe("/some/topic", handle); // 退订
  13.  
  14. //订阅
  15. $.subscribe("/some/topic", function (e, a, b, c) {
  16.     console.log(+ b + c);
  17. });
  18.  
  19. $.publish("/some/topic", ["a", "b", "c"]); // 输出abc
  20.  
  21. //退订(退订使用的是/some/topic名称,而不是回调函数哦,和版本一的例子不一样
  22. $.unsubscribe("/some/topic");

可以看到,他的订阅和退订使用的是字符串名称,而不是回调函数名称,所以即便传入的是匿名函数,我们也是可以退订的。

总结

观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。

总的来说,观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。


还没有评论.