Node的大部分对象(如HTTP请求,响应和流)都实现了EventEmitter模块,从而可以提供发送和监听事件的方法。

EventEmitter

事件驱动性质的最简单形式是一些流行的Node.js函数的回调方式,例如fs.readFile。在这个类比中,事件将被触发一次(当Node准备好调用回调函数时),回调作为事件处理程序。

我们先来探讨这个基本形式。

当你准备好的时候给我打电话,Node!

Node处理异步事件的原始方式是回调。这是很久以前,在JavaScript有本机(Promise)承诺支持和async/await功能之前。

回调基本上只是你传递给其他功能的函数。这在JavaScript中是可能的,因为函数是第一类对象。

重要的是要明白回调在代码中不表示异步调用。函数可以同步和异步地调用回调。

例如,这里是一个主机函数fileSize,它接受一个回调函数cb,并且可以基于一个条件同步和异步地调用该回调函数:

1
2
3
4
5
6
7
8
9
function fileSize (fileName, cb) {
if (typeof fileName !== 'string') {
return cb(new TypeError('argument should be string')); // Sync
}
fs.stat(fileName, (err, stats) => {
if (err) { return cb(err); } // Async
cb(null, stats.size); // Async
});
}

请注意,这是导致意外错误的不良做法。设计主机函数以始终同步或始终异步地使用回调。

我们来探讨一个典型的异步Node函数的简单示例,它使用回调样式:

1
2
3
4
5
6
7
8
9
const readFileAsArray = function(file, cb) {
fs.readFile(file, function(err, data) {
if (err) {
return cb(err);
}
const lines = data.toString().trim().split('\n');
cb(null, lines);
});
};

readFileAsArray需要一个文件路径和一个回调函数。它读取文件内容,将其拆分成行数组,并使用该数组调用回调函数。

这是一个用例。假设我们在同一目录中的文件numbers.txt包含如下内容:

1
2
3
4
5
6
10
11
12
13
14
15

如果我们有一个任务来计算该文件中的奇数,我们可以使用readFileAsArray来简化代码:

1
2
3
4
5
6
readFileAsArray('./numbers.txt', (err, lines) => {
if (err) throw err;
const numbers = lines.map(Number);
const oddNumbers = numbers.filter(n => n%2 === 1);
console.log('Odd numbers count:', oddNumbers.length);
});

代码将数字内容读入字符串数组,将其解析为数字,并计数奇数。

Node的回调风格纯粹在这里使用。回调有一个第一个错误的参数err为空,我们将回调作为主机函数的最后一个参数传递。您应该始终在您的功能中执行此操作,因为用户可能会假设。使主机函数接收回调作为其最后一个参数,并使回调期望一个错误对象作为其第一个参数。

回调的现代JavaScript替代方案

在现代JavaScript中,我们有承诺(Promise)的对象。 Promises可以替代异步API的回调。而不是将回调作为参数传递并在相同的地方处理错误,承诺对象允许我们单独处理成功和错误的情况,并且还允许我们链接多个异步调用而不是嵌套它们。

如果readFileAsArray函数支持promises,我们可以使用它,如下所示:

1
2
3
4
5
6
7
readFileAsArray('./numbers.txt')
.then(lines => {
const numbers = lines.map(Number);
const oddNumbers = numbers.filter(n => n%2 === 1);
console.log('Odd numbers count:', oddNumbers.length);
})
.catch(console.error);

而不是传入回调函数,我们在主机函数的返回值上调用了.then函数。这个.then函数通常给我们访问我们在回调版本中获得的相同的行数组,我们可以像以前一样对它进行处理。为了处理错误,我们在结果上添加一个.catch调用,当我们发生错误时,我们可以访问一个错误。

由于新的Promise对象,使现代JavaScript中的主机功能支持承诺界面更容易。这里的readFileAsArray函数修改为支持promise界面,除了它已经支持的回调接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
const readFileAsArray = function(file, cb = () => {}) {
return new Promise((resolve, reject) => {
fs.readFile(file, function(err, data) {
if (err) {
reject(err);
return cb(err);
}
const lines = data.toString().trim().split('\n');
resolve(lines);
cb(null, lines);
});
});
};

所以我们使函数返回一个Promise对象,它包裹fs.readFile异步调用。 promise对象暴露两个参数,一个resolve函数和一个reject函数。

每当我们想使用错误调用回调时,我们也使用promise reject函数,当我们想要使用数据调用回调函数时,我们也使用promise resolve函数。

在这种情况下,我们需要做的唯一其他事情是为这个回调参数设置默认值,以防代码与promise接口一起使用。我们可以在这个例子的参数中使用一个简单的默认空函数:()=> {}。

消费承诺与async/await

当需要循环异步功能时,添加承诺界面可使您的代码更容易处理。随着回调,事情变得凌乱。

承诺改善了一点,功能发生器改善了一点点。这就是说,使用异步代码的最新替代方案是使用异步功能,它允许我们将异步代码视为同步代码,使其整体上更加可读。

以下是使用async / await的readFileAsArray函数的方法:

1
2
3
4
5
6
7
8
9
10
11
async function countOdd () {
try {
const lines = await readFileAsArray('./numbers');
const numbers = lines.map(Number);
const oddCount = numbers.filter(n => n%2 === 1).length;
console.log('Odd numbers count:', oddCount);
} catch(err) {
console.error(err);
}
}
countOdd();

我们首先创建一个异步功能,这是一个正常的功能,它之前的字异步。在异步函数内部,我们调用readFileAsArray函数,就像它返回的是行变量一样,为了做这个工作,我们使用关键字await。之后,我们继续执行代码,就好像readFileAsArray调用是同步的。

要使事情运行,我们执行异步功能。这很简单,可读性更高。要处理错误,我们需要将异步调用包装在一个try / catch语句中。

使用此异步/等待功能,我们不必使用任何特殊的API(如.then和.catch)。我们只是标记功能不同,并使用纯JavaScript代码。

我们可以使用async / await功能与任何支持承诺接口的功能。但是,我们无法使用回调式异步函数(例如setTimeout)。

EventEmitter模块

EventEmitter是一个促进Node中对象之间的通信的模块。 EventEmitter是Node异步事件驱动架构的核心。 Node的许多内置模块都继承自EventEmitter。

这个概念很简单:发射体对象会发出命名事件,这些事件会导致以前注册的侦听器被调用。所以,发射器对象基本上有两个主要特征:

  • 发出名称事件。
  • 注册和注销侦听器功能。

要使用EventEmitter,我们只需创建一个扩展EventEmitter的类。

1
2
3
class MyEmitter extends EventEmitter {
}

Emitter对象是我们从基于EventEmitter的类实例化的对象:

1
const myEmitter = new MyEmitter();

在这些发射器对象的生命周期的任何时刻,我们可以使用emit函数发出我们想要的任何命名事件。

1
2
myEmitter.emit('something-happened');

发出事件是发生某种情况的信号。这种情况通常是关于发光物体的状态变化。

我们可以使用on方法添加监听器函数,并且这些监听器函数将在每次发射器对象发出关联名称事件时执行。

事件!==异步

我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const EventEmitter = require('events');
class WithLog extends EventEmitter {
execute(taskFunc) {
console.log('Before executing');
this.emit('begin');
taskFunc();
this.emit('end');
console.log('After executing');
}
}
const withLog = new WithLog();
withLog.on('begin', () => console.log('About to execute'));
withLog.on('end', () => console.log('Done with execute'));
withLog.execute(() => console.log('*** Executing task ***'));

Class WithLog 是事件发射器。它定义一个实例函数execute。此执行函数接收一个参数,任务函数,并使用log语句包装其执行。它在执行之前和之后触发事件。

要查看这里会发生什么的顺序,我们在两个命名事件上注册侦听器,最后执行一个示例任务来触发事件。

这是以下的输出:

1
2
3
4
5
Before executing
About to execute
*** Executing task ***
Done with execute
After executing

我想让你注意到上面的输出是一切都是同步发生的。这段代码没有异步。
我们先得到“执行前”行。
begin命名事件导致“关于执行”行。
实际执行行然后输出“执行任务”行。
结束命名事件然后导致“完成与执行”行
我们得到最后执行“行”。
就像普通的回调一样,不要以为事件意味着同步或异步代码。
这很重要,因为如果我们传递异步taskFunc来执行,那么发出的事件将不再准确。