11-事件
14.3.3 事件
事件是JavaScript中另一个备受瞩目且历久弥新的概念。它的概念很简单:事件发射器可以广播事件,任何愿意监听(或者“订阅”)这些事件的人都可以去做这件事。如何订阅事件呢?当然非回调莫属了。创建自己的事件系统其实很简单,即便如此,Node还是为我们提供了内建的支持。如果使用浏览器,jQuery同样提供了一个事件机制(http://api.jquery.com/category/events/)。为了改进countdown,我们通常会用Node的 EventEmitter 。虽然也可以在像 countdown 这样的函数中使用 EventEmitter ,不过实际上它的设计初衷是跟类一起使用。所以可以把 countdown 函数放在 Countdown 类中:
const EventEmitter = require('events').EventEmitter;
class Countdown extends EventEmitter {
constructor(seconds, superstitious) {
super();
this.seconds = seconds;
this.superstitious = !!superstitious;
}
go() {
const countdown = this;
return new Promise(function(resolve, reject) {
for(let i=countdown.seconds; i>=0; i--) {
setTimeout(function() {
if(countdown.superstitious && i===13)
return reject(new Error("DEFINITELY NOT COUNTING THAT"));
countdown.emit('tick', i);
if(i===0) resolve();
}, (countdown.seconds-i)*1000);
}
});
}
}
Countdown 类继承了 EventEmitter ,这样Countdown就可以“发射”事件。 Go 方法是正式开始倒计时并返回promise的地方。注意,在go函数中,我们所做的第一件事就是把 this 赋给countdown。这是因为在回调中,不论倒计时是否迷信数字,都需要this的值来获取倒计时的长度。(译者注:在基督教文化中,13是个不详的数字。作者在这里实际上开了个玩笑:如果倒计时是迷信的(countdown.superstitious === true),那么它会在数到13时报错。)这里要记住,this是一个特殊变量,它与回调中的this不是同一个东西。所以我们需要保存当前的this值,从而在promise中使用它。
神奇的事情就发生在调用 countdown.emit('tick', i) 的时候。任何想要监听 tick 事件(可以任意给它命名,“tick”听起来跟其他事件一样好)的人都可以监听它。接下来看看如何使用这经过改进后的全新countdown:
const c = new Countdown(5);
c.on('tick', function(i) {
if(i>0) console.log(i + '...');
});
c.go()
.then(function() {
console.log('GO!');
})
.catch(function(err) {
console.error(err.message);
})
EventEmitter 中的on方法允许监听事件。本例为每个 tick 事件提供了一个回调。如果 tick 的值不是0,就打印它。接下来调用go,从而开始倒计时。当倒计时结束时,在log中打印 GO! 。当然这里也可以把 GO! 放在 tick 事件的监听器中,这么做主要是为了强调事件和promise之间的区别。
使用这种方式比原始的 countdown 函数定义确实麻烦不少,但是获得了更多功能。现在就可以随心所欲地记录倒计时中的每一秒,同时当倒计时完成时也会有一个被满足的promise。
还有一件事没做:还没有解决一个迷信倒计时的实例,即使它已经拒绝了promise,却还是会跳过13继续倒计时的问题。
const c = new Countdown(15, true)
.on('tick', function(i) { //注意这里可以链式调用'on'
if(i>0) console.log(i + '...');
});
c.go()
.then(function() {
console.log('GO!');
})
.catch(function(err) {
console.error(err.message);
})
还是能获取倒数到0的所有标记(即使不打印出来)。解决这个问题有些复杂,因为已经创建好所有的超时了(当然,可以“作弊”,当创建的迷信计时器的时长大于或等于13秒时,就立刻失败,但这就达不到该例子的效果了)。为了解决这个问题,一旦发现不能继续倒计时,就必须清理所有未处理的超时:
const EventEmitter = require('events').EventEmitter;
class Countdown extends EventEmitter {
constructor(seconds, superstitious) {
super();
this.seconds = seconds;
this.superstitious = !!superstitious;
}
go() {
const countdown = this;
const timeoutIds = [];
return new Promise(function(resolve, reject) {
for(let i=countdown.seconds; i>=0; i--) {
timeoutIds.push(setTimeout(function() {
if(countdown.superstitious && i===13) {
// 清除所有 pending的timeouts
timeoutIds.forEach(clearTimeout);
return reject(new Error("DEFINITELY NOT COUNTING THAT"));
}
countdown.emit('tick', i);
if(i===0) resolve();
}, (countdown.seconds-i)*1000));
}
});
}
}