JavaScript入门教程

JavaScript简介
JavaScript语法基础
JavaScript流程控制
JavaScript函数
面向对象编程
JavaScript事件
JavaScript DOM
正则表达式
JavaScript BOM
AJAX

专题分析

浏览器兼容性
JS优化
Web前端开发规范
编辑器推荐
总结和笔记

学习助手

对象参考手册
ECMAScript分析
数据中心
QQ交流群

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 服务器,甚至是应用进程本身。以下例为证。
['room', 'moon', 'cow jumping over the moon']
    .forEach(function(name) {
        process.on('exit', function() {
        console.log('Goodnight, ' + name);
    });
});
浏览器端存在着无数的单机版PubSub 库。此外,很多MVC 框架,如Backbone.js 和Spine,都提供了自己的类EventEmitter 模块。本章稍后再更详细地讨论Backbone。

Node.js EventEmitter 对象

我们用Node 的EventEmitter 对象作为PubSub 接口的例子。

EventEmitter 有着简单而近乎最简化的设计。

要想给EventEmitter 对象添加一个事件处理器,只要以事件类型和事件处理器为参数调用on 方法即可。
emitter.on('evacuate', function(message) {
    console.log(message);
});
emit(意为“触发”)方法负责调用给定事件类型的所有处理器。举个例子,下面这行代码:
emitter.emit('evacuate');
将调用evacuate 事件的所有处理器。

请注意,这里的术语事件跟事件队列没有任何关系。

使用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 实现,于是决定在jQuery1.7 中将它们抽象为$.Callbacks①。这样就不再用数组来存储各种事
件类型对应的事件处理器,而可以转用$.Callbacks 实例。

很多PubSub 实现负责解析事件字符串以提供一些特殊功能。举个例子,你也许熟悉jQuery 的名称空间化事件:如果绑定了名称为"click.tbb" 和"hover.tbb" 的两个事件, 则简单地调用unbind(".tbb")就可以同时解绑定它们。Backbone.js 允许向"all"事件类型绑定事件处理器,这样不管发生什么事,都会导致这些事件处理器的触发。jQuery 和Backbone.js 都支持用空格隔开多个事件来同时绑定或触发多种事件类型,譬如"keypress mousemove"。

同步性

尽管PubSub 模式是一项处理异步事件的重要技术,但它内在跟异步没有任何关系。请考虑下面这段代码:
$('input[type=submit]')
    .on('click', function() { console.log('foo'); })
    .trigger('click');
console.log('bar');
这段代码的输出为:
foo
bar

这证明了click 事件的处理器因trigger 方法而立即被激活。事实上,只要触发了jQuery 事件,就会不被中断地按顺序执行其所有事件处理器。

好吧,我们要明确一点:用户点击Submit(提交)按钮时,这确实是一个异步事件。点击事件的第一个处理器会从事件队列中被触发。然而,事件处理器本身无法知道自己是从事件队列中还是从应用代码中运行的。

如果事件按顺序触发了过多的处理器,就会有阻塞线程且导致浏览器不响应的风险。更糟糕的是,如果事件处理器本身触发了事件,还很容易造成无限循环。
$('input[type=submit]')
    .on('click', function() {
        $(this).trigger('click'); //堆栈上溢!
    });
回想本章开头提到的文字处理程序的例子。用户按键时,需要发生很多事情,其中某些事还需要复杂的计算。全部做完这些事之后再返回事件队列,只会制造出响应迟钝的应用。

这个问题有一个很好的解决方案,就是对那些无需即刻发生的事情维持一个队列,并使用一个计时函数定时运行此队列中的下一项任务。

首次尝试编码的结果可能像这样:
var tasks = [];
setInterval(function() {
    var nextTask;
    if (nextTask = tasks.shift()) {
        nextTask();
    };
}, 0);
PubSub 模式简化了事件的命名、分发和堆积。任何时刻,只要直觉上认为对象会声明发生什么事情,就可以使用PubSub 这种很棒的模式。