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

介绍

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

使用观察者模式的好处:

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

_10正文(版本一)

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

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

使用方式如下:

  1. //来,订阅一个
  2. pubsub.subscribe('example1', function (topics, data) {
  3. console.log(topics + ": " + data);
  4. });
  5. //发布通知
  6. pubsub.publish('example1', 'hello world!');
  7. pubsub.publish('example1', ['test', 'a', 'b', 'c']);
  8. 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. pubsub.publish('example1', 'hello world!');
  7. pubsub.publish('example1', ['test', 'a', 'b', 'c']);
  8. pubsub.publish('example1', [{ 'color': 'blue' }, { 'text': 'hello'}]);
  9. //退订
  10. setTimeout(function () {
  11. pubsub.unsubscribe(testSubscription);
  12. }, 0);
  13. //再发布一次,验证一下是否还能够输出信息
  14. pubsub.publish('example1', 'hello again! (this will fail)');

_106版本二

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

  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. var o = new Observer;
  28. var f1 = function (data) {
  29. console.log('Robbin: ' + data + ', 赶紧干活了!');
  30. };
  31. var f2 = function (data) {
  32. console.log('Randall: ' + data + ', 找他加点工资去!');
  33. };
  34. o.subscribe(f1);
  35. o.subscribe(f2);
  36. o.update("Tom回来了!")
  37. //退订f1
  38. o.unsubscribe(f1);
  39. //再来验证
  40. 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. }

_184版本三

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

  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. var user = {
  8. vote: function (id) {
  9. var msg = '有人投票了!ID=' + id;
  10. this.publish(msg);
  11. }
  12. };
  13. observer.make(blogger);
  14. observer.make(user);

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

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

jQuery_272jQuery版本

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

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

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

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

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

_327总结

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

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

参考地址:

https://github.com/shichuan/javascript-patterns/blob/master/design-patterns/observer.html

http://www.addyosmani.com/resources/essentialjsdesignpatterns/book/#observerpatternjavascript

https://gist.github.com/661855