9月阅读周·JavaScript异步编程设计快速响应的网络应用:分布式事件,PubSub模式篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读七个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》。
当前阅读周书籍:《JavaScript异步编程设计快速响应的网络应用》。
PubSub模式
从 JavaScript诞生之日起,浏览器就允许向 DOM元素附加事件处理器,形如:
link.onclick = clickHandler;
啊哈,一目了然!只不过要提醒你一点:如果想向一个元素附加两个点击事件处理器,则必须自行用一个封装函数汇集这两个处理器。
link.onclick = function() {
clickHandler1.apply(this, arguments);
clickHandler2.apply(this, arguments);
};
这不仅冗长重复,而且也会制造出浮肿的、“全能的”处理器函数。正因为此,W3C于2000年向DOM规范中添加了addEventListener方法,而jQuery将其抽象成 bind 方法。使用 bind,很容易对任何元素或元素集合发生的任何事件添加任意多的处理器,且完全不用担心这些处理器因摩肩接踵而出现踩踏事故。
$(link)
.bind('click', clickHandler1)
.bind('click', clickHandler2);
(在jQuery1.7+中,优先使用新的on语法而不用bind。那里也提供了click方法,不过它只是bind('click',……)的简写。但是,笔者倾向于一直使用bind/on。)
从软件架构的角度看,jQuery将link元素的事件发布给了任何想订阅此事件的人。这正是称其为PubSub模式的原因。
在老式 DOM 的事件 API 中,绑定至事件意味着要编写 object.onevent=……这样的代码,但现在它差不多被人忘光了,人们都转投至PubSub的怀抱了。Node的API架构师因为太喜欢PubSub,所以决定包含一个一般性的 PubSub 实体。这个实体叫做 EventEmitter(事件发生器),其他对象可以继承它。Node中几乎所有的I/O源都是EventEmitter 对象:文件流、HTTP 服务器,甚至是应用进程本身。以下例为证。
Distributed/processExit.js
['room', 'moon', 'cow jumping over the moon']
.forEach(function(name) {
process.on('exit', function() {
console.log('Goodnight, ' + name);
});
});
浏览器端存在着无数的单机版 PubSub库。此外,很多 MVC框架,如Backbone.js和Spine,都提供了自己的类EventEmitter模块。
EventEmitter对象
我们用 Node 的 EventEmitter 对象作为 PubSub 接口的例子。EventEmitter有着简单而近乎最简化的设计。
要想给EventEmitter对象添加一个事件处理器,只要以事件类型和事件处理器为参数调用on方法即可。
emitter.on('evacuate', function(message) {
console.log(message);
});
emit(意为“触发”)方法负责调用给定事件类型的所有处理器。举个例子,下面这行代码:
emitter.emit('evacuate');
将调用evacuate事件的所有处理器。
请注意,这里的术语事件跟事件队列没有任何关系。请参阅2.1.3节。
使用emit方法触发事件时,可以添加任意多的附加参数。所有参数均传递至所有处理器。
emitter.emit('evacuate', 'Woman and children first!');
事件名称不存在任何限制,然而 Node 相关文档还是规定了一条有用的约定。
通常,事件名称会表示为一个驼峰式大小写混合的字符串。
EventEmitter 对象的所有方法都是公有的,但一般约定只能从EventEmitter 对象的“内部”触发事件。也就是说,如果有一个对象继承了 EventEmitter原型并使用了 this.emit 方法来广播事件,则不应该从这个对象之外的其他地方再调用其emit方法。
玩转自己的PubSub
PubSub 模式的实现如此简单,以至于用十几行代码就能建立自己的PubSub 实现。对于支持的每种事件类型,唯一需要存储的状态值就是一个事件处理器清单。
PubSub = {handlers:{}}
需要添加事件监听器时,只要将监听器推入数组末尾即可(这意味着总是会按照添加监听器的次序来调用监听器)。
PubSub.on = function(eventType, handler) {
if (!(eventType in this.handlers)) {
this.handlers[eventType] = [];
}
this.handlers[eventType].push(handler);
return this;
}
接着,等到触发事件的时候,再循环遍历所有的事件处理器。
PubSub.emit = function(eventType) {
var handlerArgs = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < this.handlers[eventType].length; i++) {
this.handlers[eventType][i].apply(this, handlerArgs);
}
return this;
}
就是这么简单!现在只实现了Node之EventEmitter对象的核心部分。(还没实现的重要部分只剩下移除事件处理器及附加一次性事件处理器等功能。)
当然,各种PubSub实现在特性方面会稍有不同。jQuery团队注意到jQuery库里到处都在用几个不同的PubSub实现,于是决定在jQuery 1.7中将它们抽象为$.Callbacks。这样就不再用数组来存储各种事件类型对应的事件处理器,而可以转用$.Callbacks实例。
很多PubSub实现负责解析事件字符串以提供一些特殊功能。举个例子,你也许熟悉 jQuery 的名称空间化事件:如果绑定了名称为"click.tbb"和 "hover.tbb"的两个事件,则简单地调用unbind(".tbb")就可以同时解绑定它们。Backbone.js 允许向"all"事件类型绑定事件处理器,这样不管发生什么事,都会导致这些事件处理器的触发。jQuery和 Backbone.js都支持用空格隔开多个事件来同时绑定或触发多种事件类型,譬如"keypress mousemove"。
总结
PubSub 模式简化了事件的命名、分发和堆积。任何时刻,只要直觉上认为对象会声明发生什么事情,就可以使用 PubSub这种很棒的模式。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)