Node的大部分对象(如HTTP请求,响应和流)都实现了EventEmitter
模块,从而可以提供发送和监听事件的方法。
事件驱动性质的最简单形式是一些流行的Node.js函数的回调方式,例如fs.readFile
。在这个类比中,事件将被触发一次(当Node准备好调用回调函数时),回调作为事件处理程序。
我们先来探讨这个基本形式。
当你准备好的时候给我打电话,Node!
Node处理异步事件的原始方式是回调。这是很久以前,在JavaScript有本机(Promise
)承诺支持和async
/await
功能之前。
回调基本上只是你传递给其他功能的函数。这在JavaScript中是可能的,因为函数是第一类对象。
重要的是要明白回调在代码中不表示异步调用。函数可以同步和异步地调用回调。
例如,这里是一个主机函数fileSize
,它接受一个回调函数cb
,并且可以基于一个条件同步和异步地调用该回调函数:
|
|
请注意,这是导致意外错误的不良做法。设计主机函数以始终同步或始终异步地使用回调。
我们来探讨一个典型的异步Node函数的简单示例,它使用回调样式:
|
|
readFileAsArray
需要一个文件路径和一个回调函数。它读取文件内容,将其拆分成行数组,并使用该数组调用回调函数。
这是一个用例。假设我们在同一目录中的文件numbers.txt包含如下内容:
|
|
如果我们有一个任务来计算该文件中的奇数,我们可以使用readFileAsArray
来简化代码:
|
|
代码将数字内容读入字符串数组,将其解析为数字,并计数奇数。
Node的回调风格纯粹在这里使用。回调有一个第一个错误的参数err
为空,我们将回调作为主机函数的最后一个参数传递。您应该始终在您的功能中执行此操作,因为用户可能会假设。使主机函数接收回调作为其最后一个参数,并使回调期望一个错误对象作为其第一个参数。
回调的现代JavaScript替代方案
在现代JavaScript中,我们有承诺(Promise)的对象。 Promises可以替代异步API的回调。而不是将回调作为参数传递并在相同的地方处理错误,承诺对象允许我们单独处理成功和错误的情况,并且还允许我们链接多个异步调用而不是嵌套它们。
如果readFileAsArray函数支持promises,我们可以使用它,如下所示:
|
|
而不是传入回调函数,我们在主机函数的返回值上调用了.then
函数。这个.then
函数通常给我们访问我们在回调版本中获得的相同的行数组,我们可以像以前一样对它进行处理。为了处理错误,我们在结果上添加一个.catch
调用,当我们发生错误时,我们可以访问一个错误。
由于新的Promise对象,使现代JavaScript中的主机功能支持承诺界面更容易。这里的readFileAsArray
函数修改为支持promise界面,除了它已经支持的回调接口:
|
|
所以我们使函数返回一个Promise
对象,它包裹fs.readFile
异步调用。 promise对象暴露两个参数,一个resolve
函数和一个reject
函数。
每当我们想使用错误调用回调时,我们也使用promise reject
函数,当我们想要使用数据调用回调函数时,我们也使用promise resolve
函数。
在这种情况下,我们需要做的唯一其他事情是为这个回调参数设置默认值,以防代码与promise接口一起使用。我们可以在这个例子的参数中使用一个简单的默认空函数:()=> {}。
消费承诺与async/await
当需要循环异步功能时,添加承诺界面可使您的代码更容易处理。随着回调,事情变得凌乱。
承诺改善了一点,功能发生器改善了一点点。这就是说,使用异步代码的最新替代方案是使用异步功能,它允许我们将异步代码视为同步代码,使其整体上更加可读。
以下是使用async / await的readFileAsArray
函数的方法:
|
|
我们首先创建一个异步功能,这是一个正常的功能,它之前的字异步。在异步函数内部,我们调用readFileAsArray函数,就像它返回的是行变量一样,为了做这个工作,我们使用关键字await。之后,我们继续执行代码,就好像readFileAsArray调用是同步的。
要使事情运行,我们执行异步功能。这很简单,可读性更高。要处理错误,我们需要将异步调用包装在一个try / catch语句中。
使用此异步/等待功能,我们不必使用任何特殊的API(如.then和.catch)。我们只是标记功能不同,并使用纯JavaScript代码。
我们可以使用async / await功能与任何支持承诺接口的功能。但是,我们无法使用回调式异步函数(例如setTimeout)。
EventEmitter
模块
EventEmitter
是一个促进Node中对象之间的通信的模块。 EventEmitter
是Node异步事件驱动架构的核心。 Node的许多内置模块都继承自EventEmitter。
这个概念很简单:发射体对象会发出命名事件,这些事件会导致以前注册的侦听器被调用。所以,发射器对象基本上有两个主要特征:
- 发出名称事件。
- 注册和注销侦听器功能。
要使用EventEmitter,我们只需创建一个扩展EventEmitter的类。
|
|
Emitter对象是我们从基于EventEmitter的类实例化的对象:
|
|
在这些发射器对象的生命周期的任何时刻,我们可以使用emit函数发出我们想要的任何命名事件。
|
|
发出事件是发生某种情况的信号。这种情况通常是关于发光物体的状态变化。
我们可以使用on方法添加监听器函数,并且这些监听器函数将在每次发射器对象发出关联名称事件时执行。
事件!==异步
我们来看一个例子:
|
|
Class WithLog
是事件发射器。它定义一个实例函数execute
。此执行函数接收一个参数,任务函数,并使用log语句包装其执行。它在执行之前和之后触发事件。
要查看这里会发生什么的顺序,我们在两个命名事件上注册侦听器,最后执行一个示例任务来触发事件。
这是以下的输出:
|
|
我想让你注意到上面的输出是一切都是同步发生的。这段代码没有异步。
我们先得到“执行前”行。
begin命名事件导致“关于执行”行。
实际执行行然后输出“执行任务”行。
结束命名事件然后导致“完成与执行”行
我们得到最后执行“行”。
就像普通的回调一样,不要以为事件意味着同步或异步代码。
这很重要,因为如果我们传递异步taskFunc来执行,那么发出的事件将不再准确。