笔记:《JavaScript 设计模式与开发实践》第八章


书里的第八章「发布—订阅模式」的目录结构为:

  • 第8章 发布—订阅模式
    • 8.1 现实中的发布—订阅模式  
    • 8.2 发布—订阅模式的作用  
    • 8.3 DOM 事件  
    • 8.4 自定义事件  
    • 8.5 发布—订阅模式的通用实现  
    • 8.6 取消订阅的事件  
    • 8.7 真实的例子——网站登录  
    • 8.8 全局的发布—订阅对象  
    • 8.9 模块间通信  
    • 8.10 必须先订阅再发布吗  
    • 8.11 全局事件的命名冲突  
    • 8.12 JavaScript实现发布—订阅模式的便利性  
    • 8.13 小结  

但直到了 *8.8 全局的发布—订阅对象*,前后的 code example 实际上是两种不同的对语观察者模式的实现:

  • 8.8 之前是类似 Event Emit 的实现,
  • 8.8 之后,从先订阅后发布的离线事件,到作为全局变量存在 global 对象,则是类似 Global Boardcast 的 Pub/Sub 的实现。

1. 类似 Event Emit 的实现:

这种实现方式尤其醒目的一点是,会有一个 Events 对象存在,而之后的每个对象都是从这个对象进行继承或者扩展属性和方法,也就是书里作者所使用的 installEvent 方法,而在每个对象之后的方法调用上,基本的形式会是:

myObject.on('EventName', handler);
myObject.tigger('EventName', param1, param2, ...);
myObject.off('EventName', handler);

jQuery 类似的 API 例子:

// Publish
// jQuery: $(obj).trigger("channel", [arg1, arg2, arg3]);
$( el ).trigger( "/login", [{username:"test", userData:"test"}] );

// Subscribe
// jQuery: $(obj).on( "channel", [data], fn );
$( el ).on( "/login", function( event ){...} );

// Unsubscribe
// jQuery: $(obj).off( "channel" );
$( el ).off( "/login" );

做了一个很简单的实现 ,只有 publish()subscribe() 两个方法,利用 extend() ,将 publish()subscribe() 方法扩展到新申明的对象上。只是一般实现,一般还会有 off()once() 等方法。

function PubSub () {
  this.events = {};
}

PubSub.prototype.subscribe = function (eventName, handler) {
  (this.events[eventName] = this.events[eventName] || [])
    .push(handler);
};

PubSub.prototype.publish = function (eventName) {
  for (var list = this.events[eventName], i=0; list && list[i];) {
    list[i++].apply(this, list.splice.call(arguments, 1));
  }
};

function extend (obj, extension) {
  for (var key in extension) {
    obj[key] = extension[key];
  }
  return obj;
}

var extended = extend({}, new PubSub());

extended.subscribe('customEvent1', function (a,b) {
  console.log(a+b);
});

extended.subscribe('customEvent2', function (a,b) {
  console.log(a-b);
});

extended.publish('customEvent1', 1, 2); // 3
extended.publish('customEvent2', 1, 2); // 2

(new PubSub()).publish('customEvent1', 1, 2);
// nothing happened

JS Bin on jsbin.com


2. 另一种是作为一个 Global Boardcast 的 Pub/Sub 的实现:

也就是书里最后 demo 的实现;不过书里作者的比一般的实现要复杂些,例如缓存消息的 offlineStack ,可能是讲一些平时遇到的业务需求的例子也加了进去;基本的形式会是:

globalBroadcaster.listen('EventName', handler);
globalBroadcaster.trigger('EventName', param1, param2, ...);
globalBroadcaster.remove('EventName', handler);

同样也做了一个简单的实现

var Pubsub = {};

(function (obj) {
  var events = {};

  obj.subscribe = function (eventName, handler) {
    (events[eventName] = events[eventName] || []).push(handler);
  };

  obj.publish = function (eventName) {
    for (var list = events[eventName], i=0; list && list[i];) {
      list[i++].apply(this, list.splice.call(arguments, 1));
    }
  };

}(Pubsub));

Pubsub.subscribe('customEvent1', function (a,b) {
  console.log(a+b);
});

Pubsub.subscribe('customEvent2', function (a,b) {
  console.log(a-b);
});

Pubsub.publish('customEvent1', 1, 2); // 3
Pubsub.publish('customEvent2', 1, 2); // 2

JS Bin on jsbin.com

其实和上面 event emit 的实现上并没有多大区别,直接拿上面的下来也可以,只是在这里的使用方法不会用到 extend() 方法来对新申明的对象做属性和方法的扩展,Pubsub 在这里作为一个全局对象存在,一切消息都走 Global Boardcast。


Event Emit 和 Pub/Sub 的主要区别还是在对象方法的调用上,别的其实都很像,更加详细可以参考 Addy Osmani 写的《JavaScript Design Pattern》中的 The Observer Pattern 一章,到达这一章往下拉没多少就是 Pub/Sub 的实现。


最后要说的是,8.13 那里,作者说

本章我们学习了发布-订阅模式,也就是常说的观察者模式。

这其实不太对,Pub/Sub 很好的帮助了在日常开发中的对象之间的节藕,比如自己制作手势库,用到了 Pub/Sub,将系统原生的各种 touch 事件的 event 对象当作参数传送给自定义的处理收视判断的函数。两个对象之间并没有之间的关系,使用一个 Global Boardcast 的方式相互传递各自需要的数据。但这不是观察者模式。

按照 Addy Osmani 写的《JavaScript Design Pattern》中的 The Observer Pattern 一章 中关于观察者模式给出的,做了一个简单的例子 ;主要修改了作为 Observer 的 update() 方法。

观察者模式中的 Subject 和 Observer 主要相互关系在于,Subject 可以去 add() 或者 remove() 一个 Observer,需要的时候去使用 notify() 方法,去调用 Observer 对象上的 update() 方法。,而不是上文中实现的 pub/sub。

JS Bin on jsbin.com

//Observers
var Observers = function () {
  this.observers = [];
};

Observers.prototype.add = function (observer) {
  this.observers.push(observer);
};

Observers.prototype.remove = function (observerToRemove) {
  this.observers = this.observers.filter(function (observer) {
    return observer !== observerToRemove;
  });
};

Observers.prototype.get = function () {
  return this.observers;
};
// Observer
var Observer = function (name) {
  this.name = name;
};

Observer.prototype.update = function (event, data) {
  console.log('the event was: ', event, ' the data was ', data, ' and name is ', this.name);
};
var Subject = function () {
  this.observers = new Observers();
}

Subject.prototype.observer = function (observer) {
  this.observers.add(observer);
};

Subject.prototype.unObserver = function (observer) {
  this.observers.remove(observer);
};

Subject.prototype.notify = function (event, data) {
  this.observers.get().forEach(function (observer) {
    observer.update(event, data);
  });
};

逻辑代码

var collection = extend(new Collection(), new Subject());

var observer = new Observer('observer 1');
var otherObserver = new Observer('observer 2');

collection.observer(observer);
collection.observer(otherObserver);

collection.notify('add', {prop: 'something'});

// the event was:  add  the data was  Object {prop: "something"}  and name is  observer 1
// the event was:  add  the data was  Object {prop: "something"}  and name is  observer 2

最后

第八章最后一章节的例子,包含离线消息接受和 namespace 实现的全局 Event 对象,原文中大段大段的代码没有注释(我看的是 kindle 的电子版,不知纸质的咋样),所以给加了一遍注释,还删掉些完全没有用到过但声明了的变量。

感谢阅读

你们好, 2018 年初把小站从 Jekyll 迁移到 Hugo 的过程中,删除了评论区放的 Disqus 插件,考虑有二:首先无论评论、还是对笔记内容的进一步讨论,读者们更喜欢通过邮件、或者 Twitter 私信的方式来沟通;其次一年多以来 Disqus 后台能看到几乎都是垃圾留言(spam),所以这里直接贴一下邮件、以及 Twitter 账户 地址。

技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。

BEST
Lien(A.K.A 胡椒)
本站总访问量 本站总访客量 本文总阅读量