九、事件驱动编程和内置模块
学习目标
在本章结束时,你将能够:
- 在 Node.js 中使用事件模块
- 创建事件发射器以增强现有代码的功能
- 构建自定义事件发射器
- 使用内置的模块和工具
- 实现一个定时器模块来获得一个 API 来调度定时器函数
在本章中,我们将使用事件发射器和内置模块来避免创建依赖深度耦合的项目。
简介
在前一章中,我们讨论了如何在 Node.js 中使用事件驱动编程,以及如何修改普通的基于回调的异步操作来使用异步等待和承诺。 我们知道 Node.js 的核心 API 是建立在异步驱动架构上的。 Node.js 有一个事件循环来处理大多数异步和基于事件的操作。
在 JavaScript 中,事件循环不断地运行,并从回调队列中摘要消息,以确保它正在执行正确的函数。 如果没有事件,我们可以看到代码是非常紧密耦合的。 对于一个简单的聊天室应用,我们需要这样写:
class Room {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
}
sendMessage(message) {
this.users.forEach(user => user.sendMessage(message));
}
}
如您所见,因为我们没有使用事件,所以需要保存房间中所有用户的列表。 当我们向房间添加用户时,我们还需要将用户添加到我们创建的列表中。 在发送消息时,我们还需要遍历列表中的所有用户并调用sendMessage
方法。 我们的用户类应该这样定义:
class User {
constructor() {
this.rooms = {}
}
joinRoom(roomName, room) {
this.rooms[roomName] = room;
room.addUser(this);
}
sendMessage(roomName, message) {
this.rooms[roomName].sendMessage(message);
}
}
你可以看到这变得多么复杂; 为了加入一个聊天室,我们需要同时添加房间和当前用户到房间。 当我们的应用最终变得非常复杂时,我们将看到这对传统方法提出了一个问题。 如果这个应用曾经需要网络请求(异步操作),它将变得非常复杂,因为我们需要用异步操作包装我们希望执行的所有代码。 我们也许可以把这个逻辑拉出来,但是当我们处理由未知数量的随机事件驱动的应用时,使用事件驱动编程的好处是使我们的代码更容易维护。
传统方法与事件驱动编程
正如我们在介绍中提到的,在传统的编程模式中,当我们希望组件之间进行通信时,我们希望它们之间有一个直接的链接。 如下图所示:
图 9.1:传统编程方法
对于一个允许用户更新他们的配置文件和接收消息的简单应用,我们可以看到我们有四个组件:
- 代理
- 配置文件
- 票
- 消息
这些组件之间的交互方式是通过调用希望通信的组件中的适当方法。 通过这样做,代码非常容易理解,但我们可能必须传递组件引用。 以我们的Agent
课为例:
class Agent {
constructor(id, agentInfo, voteObj, messageObj) {
this.voteObj = voteObj;
this.messageObj = messageObj;
}
checkMessage() {
if (this.messageObj.hasMessage()) {
const message = this.messageObj.nextMessate();
return message;
}
return undefined;
}
checkVote() {
if (this.voteObj.hasNewVote()) {
return true;
}
return false;
}
}
Agent
类必须存储对它将来想要通信的组件的引用。 没有它,我们的组件就无法与其他组件通信。 在前面的例子中,我们创建的Agent
对象与其他所有对象都是紧密耦合的。 当它被创建时,它需要这些对象的所有引用,这使得我们的代码很难解耦,如果我们想在未来改变一些东西。 考虑前面的Agent
代码。 如果我们要向它添加更多的特性,我们希望代理类能够与新特性通信,比如社交页面、实时流页面等等。 这在技术上是可行的,只要我们在constructor
中添加对这些对象的引用。 这样做,我们的代码在未来可能会变成这样:
class Agent {
constructor(id, agentInfo, voteObj, messageObj, socialPage, gamePage, liveStreamPage, managerPage, paymentPage...) {
this.voteObj = voteObj;
this.messageObj = messageObj;
this.socialPage = socialPage;
this.gamePage = gamePage;
this.liveStreamPage = liveStreamPage;
this.managerPage = managerPage;
this.paymentPage = paymentPage;
...
}
...
}
当我们的应用变得越来越复杂时,我们的Agent
类也变得越来越复杂。 由于它具有constructor
中的所有引用,我们可能会遇到错误地传递参数类型所导致的问题。 当我们试图同时在多个组件之间进行通信时,这是一个常见的问题。
【T0
我们之前的方法——即处理组件通信——是直接的,实际上是静态的。 我们需要存储想要与之通信的组件引用,并在想要向它发送消息时编写非常特定于组件的代码。 在 JavaScript 中,有一种新的通信方式,叫做事件。
让我们考虑这个例子; 朋友传递给你的光是一种让你接收来自朋友的事件的方式。 在 JavaScript 中,我们可以拥有能够发出事件的对象。 通过发出事件,我们可以创建对象之间通信的新方式。 这也称为观察者模式。 下图描述了观察者模式:
图 9.2:观察者模式
在此模式中,希望发起通信的组件只会发出一个事件,而不是调用组件中的特定方法。 我们可以有多个观察器来观察来自组件的事件。 这样,我们就把使用消息的责任完全交给了使用者。 当观察者决定观察事件时,它将在组件每次发出该事件时接收该事件。 如果前面的复杂示例是使用事件实现的,它将是这样的:
图 9.3:使用事件的观察者模式
在这里,我们可以看到每个组件都遵循我们的观察者模式,当我们把它转换成代码时,它看起来像这样:
class Agent {
constructor(id, agentInfo, emitter) {
this.messages = [];
this.vote = 0;
emitter.on('message', (message) => {
this.messages.push(message);
});
emitter.on('vote', () => {
this.vote += 1;
})
}
}
现在,我们没有获取我们想要通信的所有组件的所有引用,而是只传递一个事件发射器,它处理所有消息传递。 这使得我们的代码与其他组件更加分离。 这基本上就是我们在代码中实现事件观察器模式的方式。 在现实生活中,这可能会变得更加复杂。 在下一个练习中,我们将通过一个简单的例子来演示如何使用 Node.js 中的内置事件系统来触发事件。
一个简单的事件发射器
在介绍中,我们讨论了如何使用事件观察器模式来删除代码中希望通信的所有组件的引用。 在这个练习中,我们将了解 Node.js 中的内置事件模块,如何创建EventEmitter
,以及如何使用它。
执行以下步骤来完成这个练习:
-
Import the
events
module:js const EventEmitter = require('events');
我们将导入 Node.js 中内置的
events
模块。 它提供了一个构造函数,我们可以使用它来创建自定义事件发射器或创建从它继承的类。 因为这是一个内置模块,所以不需要安装。 -
创建一个新的
EventEmitter
:js const emitter = new EventEmitter();
-
尝试触发一个事件:
js emitter.emit('my-event', { value: 'event value' });
-
Attach an event listener:
js emitter.on('my-event', (value) => {
js console.log(value);
js });
要向发射器添加事件监听器,我们需要调用发射器上的
on
方法,该方法带有事件名称和事件触发时要调用的函数。 当我们在发出事件之后添加事件监听器时,我们将看到事件监听器没有被调用。 这样做的原因是,当我们之前发出事件时,没有为该事件附加事件监听器,所以它没有被调用。 -
Emit another event:
js emitter.emit('my-event', { value: 'another value' });
当我们这次发出一个事件时,我们将看到我们的事件监听器被正确调用,我们的事件值也被正确打印出来,如下所示:
图 9.4:使用正确的事件值触发的事件
-
Attach another event listener for
my-event
:js emitter.on('my-event', (value) => {
js console.log('i am handling it again');
js });
我们不局限于每个事件只有一个侦听器——我们可以附加尽可能多的事件侦听器。 当事件被触发时,它将调用所有的侦听器。
-
Emit another event:
js emitter.emit('my-event', { value: 'new value' });
下面是上述代码的输出:
图 9.5:多次发出事件后的输出
当我们再次触发事件时,我们将看到我们触发的第一个事件。 我们还将看到它成功地打印出了我们的消息。 注意,它保持了与附加侦听器时相同的顺序。 当我们发出一个错误时,发射器将遍历数组并逐个调用每个侦听器。
-
Create the
handleEvent
function:js function handleEvent(event) {
js console.log('i am handling event type: ', event.type);
js }
当我们设置事件监听器时,我们使用匿名函数。 虽然这很简单,但它并没有为我们提供
EventEmitters
提供的所有功能: -
将新的
handleEvent
附加到一种新的事件类型上:js emitter.on('event-with-type', handleEvent);
-
Emit the new event type:
js emitter.emit('event-with-type', { type: 'sync' });
下面是上述代码的输出:
图 9.6:发出新的事件类型
-
Remove the event listener:
js emitter.removeListener('event-with-type', handleEvent);
因为我们使用的是一个命名函数,所以一旦不再需要将事件传递给侦听器,就可以使用这个函数引用来删除侦听器。
-
Emit the event after the listener has been removed:
js emitter.emit('event-with-type', { type: 'sync2' });
下面是上述代码的输出:
图 9.7:删除侦听器后 emit 事件的输出
因为我们刚刚删除了event-with-type
的侦听器,所以当我们再次发出事件时,它将不会被调用。
在这个练习中,我们构建了一个非常简单的事件发射器,并测试了添加和删除侦听器。 现在,我们知道了如何使用事件将消息从一个组件传递到另一个组件。 接下来,我们将深入研究事件侦听器方法,并看看通过调用它们可以完成什么。
EventEmitter 方法
在前面的练习中,我们讨论了两个可以调用的方法,用来触发事件和附加侦听器。 我们还使用了removeListener
来删除我们所附加的侦听器。 现在,我们将介绍事件监听器上可以调用的各种方法。 这将帮助我们更容易地管理事件发射器。
删除监听器
在某些情况下,我们希望从发射器中删除侦听器。 与前面的练习一样,我们可以通过调用removeListener
来删除侦听器:
emitter.removeListener('event-with-type', handleEvent);
当调用removeListener
方法时,必须为其提供事件名称和函数引用。 当我们调用这个方法时,是否设置了事件监听器并不重要; 如果监听器没有设置为 begin,则什么也不会发生。 如果设置了,它将遍历事件发射器中的监听器数组,并删除该监听器的第一次出现,如下所示:
const emitter = new EventEmitter();
function handleEvent(event) {
console.log('i am handling event type: ', event.type);
}
emitter.on('event-with-type', handleEvent);
emitter.on('event-with-type', handleEvent);
emitter.on('event-with-type', handleEvent);
emitter.emit('event-with-type', { type: 'sync' });
emitter.removeListener('event-with-type', handleEvent);
在这个代码中,我们将同一个监听器连接了三次。 当我们附加事件监听器时,这在事件发射器中是允许的; 它只是被附加到该事件的事件侦听器数组中。 当我们在removeListener
之前触发事件时,我们会看到监听器被调用了三次:
图 9.8:在删除侦听器之前,侦听器使用 emit 事件调用了三次
在本例中,因为我们的事件有三个相同的监听器,当我们调用removeListener
时,它只会删除listener
数组中的第一个监听器。 当我们再次触发相同的事件时,我们将看到它只运行两次:
图 9.9:使用 removeListener 后,第一个侦听器被删除
删除所有监听器
我们可以从事件发射器中移除特定的监听器。 但通常,当我们在发射器上处理多个监听器时,我们会想要删除所有监听器。 类为我们提供了一个方法,我们可以使用它来删除特定事件的所有侦听器。 考虑我们之前使用的同一个例子:
const emitter = new EventEmitter();
function handleEvent(event) {
console.log('i am handling event type: ', event.type);
}
emitter.on('event-with-type', handleEvent);
emitter.on('event-with-type', handleEvent);
emitter.on('event-with-type', handleEvent);
如果我们想要删除event-with-type
事件的所有监听器,我们必须多次调用removeListener
。 有时,当我们确定所有的事件监听器都是我们添加的,而没有其他组件或模块时,我们可以使用一个方法调用来删除该事件的所有监听器:
emitter.removeAllListeners('event-with-type');
当我们调用removeAllListeners
时,我们只需要提供事件名称。 这将删除附加到该事件的所有侦听器。 调用它之后,事件将没有处理程序。 如果你正在使用这个,请确保你没有删除其他组件附加的监听器:
emitter.emit('event-with-type', { type: 'sync' });
当我们在调用removeAllListeners
之后再次触发同样的事件时,我们将看到程序将什么也不输出:
图 9.10:使用 removeAllListeners 不会输出任何内容
附加一次性监听器
有时,我们希望组件只接收一次特定事件。 我们可以通过使用removeListener
来实现这一点,以确保我们在调用后删除了侦听器:
const EventEmitter = require('events');
const emitter = new EventEmitter();
function handleEvent(event) {
console.log('i am handling event type once : ', event.type);
emitter.removeListener('event-with-type', handleEvent);
}
emitter.on('event-with-type', handleEvent);
emitter.emit('event-with-type', { type: 'sync' });
emitter.emit('event-with-type', { type: 'sync' });
emitter.emit('event-with-type', { type: 'sync' });
在这里,我们可以看到,在我们的handleEvent
监听器中,我们也在执行监听器之后删除了它。 通过这种方式,我们可以确保事件侦听器只被调用一次。 当我们运行前面的代码时,我们会看到这样的输出:
图 9.11:使用 handleEvent 监听器后的输出
这做了我们想要的,但还不够好。 它要求我们在事件监听器中保留发射器的引用。 而且,它也不够健壮,因为我们无法将侦听器逻辑分离到不同的文件中。 EventEmitter
类为我们提供了一个非常简单的方法,可用于附加一次性侦听器:
...
emitter.once('event-with-type', handleEvent);
emitter.emit('event-with-type', { type: 'sync' });
emitter.emit('event-with-type', { type: 'sync' });
emitter.emit('event-with-type', { type: 'sync' });
这里,当我们附加事件监听器时,我们使用了.once
方法。 这告诉发射器,我们传递的函数只应该被调用一次,并且在调用之后将从事件监听器列表中删除。 当我们运行它时,它将为我们提供与之前相同的输出:
图 9.12:使用.once 方法获取一次性监听器
这样,我们就不需要在监听器中保留对事件发射器的引用。 这使得我们的代码更加灵活和易于模块化。
从事件发射器读取
到目前为止,我们一直在设置和删除事件发射器中的监听器。 EventEmitter
类还为我们提供了几个 read 方法,我们可以在这些方法中获得关于事件发射器的更多信息。 考虑下面的例子:
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('event 1', () => {});
emitter.on('event 2', () => {});
emitter.on('event 2', () => {});
emitter.on('event 3', () => {});
这里,我们向发射器添加了三种类型的事件监听器。 对于event 2
,我们设置两个监听器。 要获取发射器中某个事件的事件监听器的数量,可以调用listenerCount
。 对于上面的例子,如果我们想知道附加到event 1
上的事件监听器的数量,可以执行以下命令:
emitter.listenerCount('event 1');
下面是上述代码的输出:
图 9.13:输出显示了附加到事件 1 的事件数量
类似地,我们可以通过执行以下命令来检查附加到event 2
上的事件监听器的数量:
emitter.listenerCount('event 2');
下面是上述代码的输出:
图 9.14:输出显示了附加到事件 2 的事件数量
有时,我们想知道附加到事件的事件监听器列表,以便确定某个处理程序是否已经附加,如下所示:
function anotherHandler() {}
emitter.on('event 4', () => {});
emitter.on('event 4', anotherHandler);
在这里,我们将一个匿名函数附加到event 4
和另一个使用命名函数的侦听器。 如果我们想知道anotherHandler
是否已经附加到event 4
,我们可以附加一个监听器列表到该事件。 EventEmitter
类为我们提供了一个非常简单的方法来调用它:
const event4Listeners = emitter.listeners('event 4');
下面是上述代码的输出:
图 9.15:使用 EventEmitter 类获取附加到事件的监听器列表
在这里,我们可以看到已经连接到发射器的两个侦听器:一个是匿名函数,另一个是命名函数anotherHandler
。 为了检查我们的处理器是否已经附加到发射器上,我们可以检查anotherHandler
是否在event4Listeners
数组中:
event4Listeners.includes(anotherHandler);
下面是上述代码的输出:
图 9.16:检查处理程序是否附加到发射器
通过在包含方法的数组中使用此方法,我们可以确定一个函数是否已经附加到事件中。
获取注册了监听器的事件列表
还有些时候,我们需要获取已注册了侦听器的事件列表。 这可用于确定是否已将监听器附加到事件,或查看事件名称是否已被占用。 继续前面的例子,我们可以通过调用EventEmitter
类中的另一个内部方法来获得该信息:
emitter.eventNames();
下面是上述代码的输出:
图 9.17:使用 EventEmitter 类获取事件名称的信息
在这里,我们可以看到事件发射器有附加到四种不同事件类型的监听器; 即事件 1-4。
最大的听众
默认情况下,每个事件发射器最多只能为任何单个事件注册 10 个侦听器。 当我们附加的值超过最大值时,我们会得到这样的警告:
图 9.18:当为单个事件附加超过 10 个监听器时发出警告
这是一种预防措施,以确保我们没有内存泄漏,但有时我们需要为一个事件设置超过 10 个侦听器。 如果我们确定,我们可以通过调用setMaxListeners
来更新默认最大值:
emitter.setMaxListeners(20)
这里,我们将最大侦听器默认设置为20
。 我们还可以将其设置为0
或 Infinity,以允许无限数量的监听器。
前置听众
当我们添加侦听器时,它们被附加到侦听器数组的末尾。 当触发事件时,发射器将按分配的顺序调用每个分配的侦听器。 在某些情况下,我们需要首先调用侦听器,我们可以使用事件发射器提供的内置方法来实现这一点:
const EventEmitter = require('events');
const emitter = new EventEmitter();
function handleEventSecond() {
console.log('I should be called second');
}
function handleEventFirst() {
console.log('I should be called first');
}
emitter.on('event', handleEventSecond);
emitter.on('event', handleEventFirst);
emitter.emit('event');
在这里,我们在handleEventFirst
之前附加handleEventSecond
。 当我们发出事件时,我们将看到以下输出:
图 9.19:在第一个事件之前附加第二个事件之后触发事件
因为事件监听器是按其附加的顺序调用的,所以我们可以看到,当我们发出事件时,首先调用handleEventSecond
,然后调用handleEventFirst
。 如果我们想先调用handleEventFirst
而不修改emitter.on()
附加时的顺序,可以调用prependListener
:
...
emitter.on('event', handleEventSecond);
emitter.prependListener('event', handleEventFirst);
emitter.emit('event');
前面的代码将产生以下输出:
图 9.20:使用 prependListener 对事件进行排序
这可以帮助我们保持侦听器的顺序,并确保高优先级的侦听器总是最先被调用。 接下来我们将讨论侦听器中的并发性。
监听器并发
在前几章中,我们提到了如何将多个监听器附加到发射器,以及在触发事件时这些监听器是如何工作的。 稍后,我们还讨论了如何添加侦听器,以便在触发事件时首先调用它们。 我们想要添加侦听器的原因是当侦听器被调用时,它们会被一个接一个地同步调用。 考虑下面的例子:
const EventEmitter = require('events');
const emitter = new EventEmitter();
function slowHandle() {
console.log('doing calculation');
for(let i = 0; i < 10000000; i++) {
Math.random();
}
}
function quickHandle() {
console.log('i am called finally.');
}
emitter.on('event', slowHandle);
emitter.on('event', quickHandle);
emitter.emit('event');
在这里,我们有两个连接到event
类型的监听器。 当事件被触发时,它将首先调用slowHandle
,然后调用quickHandle
。 在slowHandle
中,我们有一个非常大的循环,模拟一个非常耗时的操作,您可以在事件监听器中执行。 当我们运行前面的代码时,首先会看到打印出doing calculation
,然后会有很长时间的等待,直到调用I am called finally
。 我们可以看到,当发射器调用事件监听器时,它是同步进行的。 这可能会给我们带来问题,因为在大多数情况下,我们不希望等待一个侦听器结束后才触发另一个侦听器。 不过,有一种简单的方法可以解决这个问题:我们可以用setImmediate
函数包装代价高昂的逻辑。 setImmediate
函数将把我们的逻辑包装到一个立即执行的异步块中,这意味着耗时的循环是无阻塞的。 我们将在本书后面介绍setImmediate
函数:
...
function slowHandle() {
console.log('doing calculation');
setImmediate(() => {
for(let i = 0; i < 10000000; i++) {
Math.random();
}
});
}
当我们用setImmediate()
包装昂贵的逻辑时,代码输出进行和的计算,最后几乎同时调用。 通过用setImmediate
包装所有逻辑,我们可以确保它是异步调用的。
自定义事件发射器
在某些情况下,我们希望将事件发出功能构建到自己的自定义类中。 我们可以通过使用JavaScript ES6继承来实现。 这允许我们创建一个自定义类,同时扩展事件发射器的所有功能。 例如,假设我们正在为火灾警报构建一个类:
class FireAlarm {
constructor(modelNumber, type, cost) {
this.modelNumber = modelNumber;
this.type = type;
this.cost = cost;
this.batteryLevel = 10;
}
getDetail() {
return '${this.modelNumber}:[${this.type}] - $${this.cost}';
}
test() {
if (this.batteryLevel > 0) {
this.batteryLevel -= 0.1;
return true;
}
return false;
}
}
在这里,我们有一个带有构造函数的FireAlarm
类,该构造函数存储有关此火灾警报的信息。 它还有几个用于测试警报的自定义方法,例如检查电池电量,以及一个返回表示警报信息的字符串的getDetail
方法。 定义这个类之后,我们可以这样使用FireAlarm
类:
const livingRoomAlarm = new FireAlarm('AX-00101', 'BATT', '20');
console.log(livingRoomAlarm.getDetail());
下面是上述代码的输出:
图 9.21:定义火警类
现在,我们想要在刚刚创建的火灾报警器上设置事件。 一种方法是创建一个通用事件发射器,并将其存储在我们的FireAlarm
对象中:
class FireAlarm {
constructor(modelNumber, type, cost) {
this.modelNumber = modelNumber;
this.type = type;
this.cost = cost;
this.batteryLevel = 10;
this.emitter = new EventEmitter();
}
...
}
当我们想看警报上的事件时,我们必须这样做:
livingRoomAlarm.emitter.on('low-battery', () => {
console.log('battery low');
});
虽然这非常好,并且将适用于我们的用例,但这肯定不是最健壮的解决方案。 因为我们的火灾警报是触发事件的,所以我们想要这样的东西:
livingRoomAlarm.on('low-battery', () => {
console.log('battery low');
});
通过直接在火灾报警器上使用.on
,我们可以告诉未来的开发人员,我们的火灾报警器也是一个事件发射器。 但是现在,我们的类定义不允许使用。 我们可以通过使用类继承来解决这个问题,我们可以让我们的FireAlarm
类扩展EventEmitter
类。 通过这样做,它将拥有EventEmitter
的所有功能。 我们可以这样修改类:
class FireAlarm extends EventEmitter {
constructor(modelNumber, type, cost) {
this.modelNumber = modelNumber;
this.type = type;
this.cost = cost;
this.batteryLevel = 10;
}
...
}
通过使用关键字extends
和EventEmitter
,我们告诉 JavaScriptFireAlarm
类是EventEmitter
的子类。 因此,它将从父节点继承所有属性和方法。 但这并不能解决所有问题。 当我们使用更新的FireAlarm
运行代码时,我们会看到抛出一个错误:
图 9.22:当我们用更新的 firearm 运行代码时抛出一个错误
这是因为我们使用了一个非常自定义的类,带有自定义的构造函数,并访问了this
(这被用作当前对象的引用)。 我们需要确保在此之前调用了父构造函数。 为了让这个错误消失,我们只需在自己的构造函数中添加一个对父构造函数的调用:
class FireAlarm extends EventEmitter {
constructor(modelNumber, type, cost) {
super();
this.modelNumber = modelNumber;
this.type = type;
this.cost = cost;
this.batteryLevel = 10;
}
...
}
现在,让我们测试我们自己的自定义EventEmitter
:
livingRoomAlarm.on('low-battery', () => {
console.log('battery low');
});
livingRoomAlarm.emit('low-battery');
下面是上述代码的输出:
图 9.23:正确触发“低电量”事件的事件监听器
在这里,我们可以看到我们像处理常规的EventEmitter
一样处理livingRoomAlarm
,当我们发出低电量事件时,我们看到该事件的事件监听器被正确触发。 在下一个练习中,我们将使用我们所学的EventEmitters
来制作一个非常简单的聊天室应用。
Exercise 68: Building A Chatroom Application
前面,我们讨论了如何附加事件监听器并在事件发射器上发出事件。 在这个练习中,我们将构建一个简单的聊天室管理软件来与事件进行通信。 我们将创建多个组件,并看看如何使它们相互通信。
注意:
这个练习的代码文件可以在https://github.com/TrainingByPackt/Professional-JavaScript/tree/master/Lesson09/Exercise68中找到。
执行以下步骤来完成这个练习:
-
Create a
User
class:js class User {
js constructor(name) {
js this.name = name;
js this.messages = [];
js this.rooms = {};
js }
js joinRoom(room) {
js room.on('newMessage', (message) => {
js this.messages.push(message);
js });
js this.rooms[room.name] = room;
js }
js getMesssages(roomName) {
js return this.messages.filter((message) => {
js return message.roomName === roomName;
js })
js }
js printMessages(roomName) {
js this.getMesssages(roomName).forEach((message) => {
js console.log(`>> [${message.roomName}]:(${message.from}): ${message.message}`);
js });
js }
js sendMessage(roomName, message) {
js this.rooms[roomName].emit('newMessage', {
js message,
js roomName,
js from: this.name
js });
js }
js }
在这里,我们为用户创建了一个
User
类。 它有一个joinRoom
方法,我们可以调用该方法将该用户加入到一个房间。 它还有一个sendMessage
方法,将信息发送给房间里的每个人。 当我们加入一个房间时,我们还会侦听来自该房间的所有新消息事件,并在收到消息时追加消息。 -
Create a
Room
class that extends theEventEmitter
class:js class Room extends EventEmitter {
js constructor(name) {
js super();
js this.name = name;
js }
js }
在这里,我们通过扩展现有的
EventEmitter
类创建了一个新的Room
类。 我们这样做的原因是我们想要在我们的room
对象上有我们自己的自定义属性,这在我们的代码中创造了更多的灵活性。 -
创建两个用户:
bob
、kevin
:js const bob = new User('Bob');
js const kevin = new User('Kevin');
-
js const lobby = new Room('Lobby');
Create a room using our class:
Room
class: 5. 加入bob
、kevin
至lobby
:js bob.joinRoom(lobby);
js kevin.joinRoom(lobby);
-
从
bob
发送一些消息:js bob.sendMessage('Lobby', 'Hi all');
js bob.sendMessage('Lobby', 'I am new to this room.');
-
Print the message log for
bob
:js bob.printMessages('Lobby');
下面是上述代码的输出:
图 9.24:打印 bob 的消息日志
在这里,您可以看到我们的所有消息都被正确地添加到
bob
的日志中。 接下来,我们将检查kevin
的日志。 -
Print the message log for
kevin
:js kevin.printMessage('Lobby');
下面是上述代码的输出:
图 9.25:打印 kevin 的消息日志
尽管我们从未明确地对
kevin
做过任何事情,但他正在接收所有的消息,因为他正在房间里听一个新的消息事件。 -
Send messages from
kevin
andbob
:js kevin.sendMessage('Lobby', 'Hi bob');
js bob.sendMessage('Lobby', 'Hey kevin');
js kevin.sendMessage('Lobby', 'Welcome!');
-
Check the message log for
kevin
:js kevin.printMessages('Lobby');
下面是上述代码的输出:
图 9.26:查看 kevin 的消息日志
在这里,我们可以看到所有的消息都被正确地添加到我们的
user
对象中。 因为我们使用了事件发射器,所以避免了传递接收者的引用。 此外,因为我们在房间中发出消息事件,而用户只是侦听该事件,所以我们不需要手动遍历房间中的所有用户并传递消息。 -
Let's modify
joinRoom
andconstructor
so that we can remove the listener later:js class User {
js constructor(name) {
js this.name = name;
js this.messages = [];
js this.rooms = {};
js this.messageListener = (message) => {
js this.messages.push(message);
js }
js }
js joinRoom(room) {
js this.messageListener = (message) => {
js this.messages.push(message);
js }
js room.on('newMessage', this.messageListener);
js this.rooms[room.name] = room;
js }
js ...
js }
当我们删除侦听器时,我们需要传递该侦听器函数的引用,因此,我们需要将该引用存储在对象中,以便以后可以使用它来删除侦听器。
-
Add
leaveRoom
:js class User {
js ...
js leaveRoom(roomName) {
js this.rooms[roomName].removeListener('newMessage', this.messageListener);
js delete this.rooms[roomName];
js }
js }
在这里,我们使用在构造函数中设置的函数引用,并将其传递给房间的
removeListener
。 我们还删除了对象中的引用,以便以后可以在内存中释放它。 -
从
room
中取出bob
:js bob.leaveRoom('Lobby');
-
从
kevin
发送消息:js kevin.sendMessage('Lobby', 'I got a good news for you guys');
-
Check the message list for
bob
:js bob.printMessages('Lobby');
下面是上述代码的输出:
图 9.27:检查 bob 的消息列表
因为
bob
离开了房间,并且我们删除了消息侦听器,所以当触发新消息事件时,不会再次调用newMessage
事件处理程序。 -
Check the message list for
kevin
:js kevin.printMessages('Lobby');
下面是上述代码的输出:
图 9.28:再次检查 kevin 的消息列表
当我们检查kevin
的消息列表时,我们应该仍然能够看到他仍然收到来自房间的新消息。 如果使用传统的方法来完成,我们将需要编写更多的代码来完成同样的事情,这将非常容易出错。
在这个练习中,我们用 Node.js 构建了一个带有事件的模拟聊天应用。 我们可以看到在 Node.js 中传递事件是多么容易,以及如何正确使用它。 事件驱动编程并不适用于每个应用,但当我们需要将多个组件连接在一起时,用事件实现逻辑要容易得多。 前面的代码仍然可以改进,我们可以添加通知房间当用户离开一个房间,我们可以添加检查而添加和删除房间,以确保我们没有添加重复的房间,确保我们只删除我们的房间。 请自行扩展此功能。
在这一章中,我们学习了如何使用事件来管理应用中组件之间的通信。 在下一个活动中,我们将构建一个事件驱动的模块。
活动 13:构建事件驱动模块
假设您正在为一家软件公司工作,该公司为烟雾探测器构建模拟器。 你需要构建一个烟雾探测器模拟器,当探测器的电池下降到一定水平以下时,它会发出警报。 以下是要求:
- 探测器需要发射一个
alarm event
。 - 当电池电量低于 0.5 单位时,感烟探测器需要发出低电量事件。
- 每个烟雾探测器在最初创建时都有 10 个单位的电池。
- 烟雾探测器上的测试函数将返回 true,如果电池水平高于 0,如果低于 0,则返回 false。 每次运行测试功能时,电池将减少 0.1 个单位。
- 您需要修改提供的
House
类,以添加addDetector
和demoveDetector
方法。 addDetector
将取一个检测器对象,并为告警事件附加一个监听器,然后在发出电池电量不足和告警事件之前打印出来。removeDetector
方法将使用检测器对象并删除侦听器。
执行以下步骤来完成此活动:
- 打开
event.js
文件并找到现有的代码。 然后,修改并添加您自己的更改。 - 导入
events
模块。 - 创建扩展
EventEmitter
的SmokeDetector
类,并将batteryLevel
设置为10
。 - 在
SmokeDetector
类中创建一个test
方法来发出电池电量不足消息。 - 创建
House
类,它将存储警报的实例。 - 在
House
类中创建一个addDetector
方法,该方法将附加事件监听器。 - 创建一个
removeDetector
方法,它将帮助我们删除前面附加的告警事件监听器。 - 创建一个名为
myHouse.
的House
实例 - 创建一个名为
detector
的SmokeDetector
实例。 - 将检测器加到
myHouse.
- 创建一个循环来调用测试函数 96 次。
- 在
detector
对象上发出警报。 - 将检测器从
myHouse
物体上移除。 -
Test it to emit alarms on the detector.
请注意
这个活动的解决方案可以在 617 页找到。
在这个活动中,我们学习了如何使用事件驱动编程建模烟雾探测器。 通过使用这种方法,我们消除了在House
对象中存储多个实例的需要,并避免了使用许多行代码进行交互。
在这一节中,我们讨论了充分使用事件系统来帮助我们管理应用中的复杂通信的方法。 在下一节中,我们将介绍一些使用事件发射器的最佳实践。
事件驱动编程最佳实践
在前一章中,我们提到了使用事件发射器和事件发射器继承创建事件驱动组件的方法。 但通常情况下,您的代码需要的不仅仅是能够正确工作。 拥有一个更好管理的代码结构不仅可以让我们的代码看起来不那么混乱,还可以帮助我们避免在未来犯一些可以避免的错误。 在本节中,我们将介绍在代码中处理事件时的一些最佳实践。
回想一下我们在本章开始时讲过的内容,我们可以使用EventEmitter
对象传递事件:
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.emit('event');
当我们想要使用我们已经创建的事件发射器时,我们需要有它的引用,以便我们可以附加监听器并在稍后想要发出事件时调用发射器上的emit
函数。 这可能会导致我们的源代码非常大,这将使未来的维护非常困难:
const EventEmitter = require('events');
const userEmitter = new EventEmitter();
const registrationEmitter = new EventEmitter();
const votingEmitter = new EventEmitter();
const postEmitter = new EventEmitter();
const commentEmitter = new EventEmitter();
userEmitter.on('update', (diff) => {
userProfile.update(diff);
});
registrationEmitter.on('user registered:activated', (user) => {
database.add(user, true);
});
registrationEmitter.on('user registered: not activated', (user) => {
database.add(user, false);
});
votingEmitter.on('upvote', () => {
userProfile.addVote();
});
votingEmitter.on('downvote', () => {
userProfile.removeVote();
});
postEmitter.on('new post', (post) => {
database.addPost(post);
});
postEmitter.on('edit post', (post) => {
database.upsertPost(post);
});
commentEmitter.on('new comment', (comment) => {
database.addComment(comment.post, comment);
});
为了能够使用我们的发射器,我们需要确保发射器在当前范围内是可访问的。 一种方法是创建一个文件来保存所有发送器和附加事件监听器的逻辑。 虽然这大大简化了我们的代码,但我们将创建非常大的源代码,这将迷惑未来的开发人员,甚至可能是我们自己。 为了使代码更加模块化,我们可以从将所有侦听器函数拖放到它们各自的文件中开始。 考虑以下巨大的源代码:
// index.js
const EventEmitter = require('events');
const userEmitter = new EventEmitter();
const registrationEmitter = new EventEmitter();
const votingEmitter = new EventEmitter();
const postEmitter = new EventEmitter();
const commentEmitter = new EventEmitter();
// Listeners
const updateListener = () => {};
const activationListener = () => {};
const noActivationListener = () => {};
const upvoteListener = () => {};
const downVoteListener = () => {};
const newPostListener = () => {};
const editPostListener = () => {};
const newCommentListener = () => {};
userEmitter.on('update', updateListener);
registrationEmitter.on('user registered:activated', activationListener);
registrationEmitter.on('user registered: not activated', noActivationListener);
votingEmitter.on('upvote', upvoteListener);
votingEmitter.on('downvote', downVoteListener);
postEmitter.on('new post', newPostListener);
postEmitter.on('edit post', editPostListener);
commentEmitter.on('new comment', newCommentListener);
通过这样做,我们大大减少了代码的文件大小。 但我们可以做得更多。 保持代码组织的一种方法是将所有发射器放在一个文件中,然后在需要时导入它。 我们可以通过创建一个名为emitters.js
的文件,并将所有发射器存储在该文件中:
// emitters.js
const EventEmitter = require('events');
const userEmitter = new EventEmitter();
const registrationEmitter = new EventEmitter();
const votingEmitter = new EventEmitter();
const postEmitter = new EventEmitter();
const commentEmitter = new EventEmitter();
module.exports = {
userEmitter,
registrationEmitter,
votingEmitter,
postEmitter,
commentEmitter
};
我们在这里所做的是在一个文件中创建所有的发射器,并将该emitter
文件设置为 exports 模块。 通过这样做,我们可以把所有的发射器放在一个地方,然后,当我们使用发射器时,我们可以导入文件。 这将我们的代码更改为以下代码:
// index.js
// Emitters
const {
userEmitter,
registrationEmitter,
votingEmitter,
postEmitter,
commentEmitter
} = require('./emitters.js');
... rest of index.js
现在,当我们导入emitter.js
时,我们可以使用对象重组来选择我们想要的发射器。 我们可以在一个文件中有多个发射器,我们可以在需要的时候选择一个。 当我们想要在userEmitter
上发出一个事件时,我们需要做的就是将发射器导入到我们的代码中,并发送该事件:
const { userEmitter } = require('./emitters.js');
function userAPIHandler(request, response) {
const payload = request.payload;
const event = {
diff: payload.diff
};
userEmitter.emit('update', event);
}
我们可以看到,每当我们想要使用userEmitter
,我们可以导入我们的emitter
文件。 这也适用于我们想要附加侦听器的时候:
const { userEmitter } = require('./emitters.js');
userEmitter.on('update', (diff) => {
database.update(diff);
})
当我们将发射器分成不同的文件时,我们不仅使代码更小,而且使它更模块化。 通过将我们的发射器拉到一个单独的文件中,如果我们想在将来访问我们的发射器,就可以很容易地重用该文件。 通过这样做,我们不需要在函数中传递发送器,从而确保函数声明不会混乱。
Node.js 内置模块
在前一节中,我们广泛地学习了events
模块,并学习了关于在应用中使用事件实现简单通信的所有内容。 events
模块是 Node.js 提供的内置模块,不需要使用npm
安装。 在本模块中,我们将讨论如何使用fs
,path
和util
模块。
【T0
path
模块是一个内置模块,它提供了一些实用工具,可以帮助我们处理文件路径和文件名。
path.join(…
当我们在应用中处理目录和文件时,Path.join()
是一个非常有用的函数。 它允许我们将路径连接在一起,并输出一个可以在fs模块中使用的路径字符串。 要使用join
路径,我们可以调用join
方法,并为其提供一个路径列表。 让我们看看下面的例子:
const currentDir = '/usr/home/me/Documents/project';
const dataDir = './data';
const assetDir = './assets';
如果我们想访问当前目录中的 data 目录,我们可以使用path.join
函数将不同的路径组合成一个字符串:
const absoluteDataDir = path.join(currentDir, dataDir);
下面是上述代码的输出:
图 9.29:使用路径 Join 函数用于组合不同的路径
如果您熟悉 POSIX 系统如何标识当前目录和父目录,那么它还可以与..
和.
一起工作。 ..
表示父目录,.
表示当前目录。 例如,下面的代码可以给出当前目录的父目录的路径:
const parentOfProject = path.join(currentDir, '..');
下面是上述代码的输出:
图 9.30:显示当前目录的父目录
路径解析(path)
当我们想要获得关于文件路径的信息时,我们可以使用path.parse()
函数来获得它的根目录、基本目录、文件名和扩展名。 让我们看看下面的例子:
const myData = '/usr/home/me/Documents/project/data/data.json';
如果我们想要解析这个文件路径,我们可以使用myData
字符串调用path.parse
来获得不同的路径元素:
path.parse(myData);
这将生成以下输出:
图 9.31:使用路径解析的文件路径 解析函数
在这里,我们可以看到我们的文件路径包含一个基本名称为data.json
的文件名。 扩展名是.json
,文件名是data
。 它还解析了文件所在的目录。
path.format(path)
在前面的parse
函数中,我们设法将文件路径解析为它所尊重的组件。 我们可以使用path.format
将该信息合并到单个字符串路径中。 让我们来看看:
path.format({
dir: '/usr/home/me/Pictures',
name: 'me',
ext: '.jpeg'
});
下面是上述代码的输出:
图 9.32:使用 path.format 将信息组合成单个字符串路径
这将给出我们提供给它的组件的文件路径。
【中文翻译
fs模块是一个内置模块,为您提供 api,以便您可以与主机文件系统进行交互。 当我们需要在应用中处理文件时,它非常有用。 在本节中,我们将讨论如何在应用中使用async
和await
的fs模块。 稍后,我们将讨论最近添加的fs.promises
API,它提供了相同的功能,但返回一个承诺而不是使用回调。
请注意
在本节中,我们将使用 POSIX 系统。 如果您使用的是 Windows 系统,请确保将文件路径更新为 Windows 等效文件。 要将 fs 模块导入到代码中,请执行以下命令:
const fs = require('fs');
【中文翻译】 createeadstream (path, options)
当我们在 Node.js 中处理大文件时,建议总是使用stream
。 要创建一个读流,我们可以调用fs.createReadStream
方法。 它将返回一个流对象,我们可以将其附加到事件处理程序,以便它们获得文件的内容:
const stream = fs.createReadStream('file.txt', 'utf-8');
【中文翻译】 createWriteStream(path, options)
它的工作原理类似于createReadStream
,但它创建了一个可写流,我们可以使用它将内容流到它:
const stream = fs.createWriteStream('output', 'utf-8');
【中文翻译】 stat(path, callback)
当我们需要关于正在访问的文件的详细信息时,fs.stat
方法是非常有用的。 我们还看到许多开发人员在调用、打开、读取或写入文件之前使用fs.stat
检查文件是否存在。 虽然使用stat
检查文件的存在不会产生任何新问题,但不建议这样做。 我们应该只使用我们正在使用的函数返回的错误; 这将消除任何额外的逻辑层,并可以减少 API 调用的数量。
考虑下面的例子:
const fs = require('fs');
fs.stat('index.js', (error, stat) => {
console.log(stat);
});
这将给我们类似如下的输出:
图 9.33:使用 fs 后的输出 统计方法
【中文翻译】 readFile(path, options, callback)
这是大多数人都熟悉的函数。 当提供文件路径时,该方法将尝试读取文件的全部内容。 它将以异步的方式完成,并且回调函数将被调用文件的全部内容。 当文件不存在时,回调函数将被调用并返回错误。
考虑下面的例子:
const fs = require('fs');
fs.readFile('index.js', (error, data) => {
console.log(data);
});
这将给我们以下输出:
图 9.34:使用 fs 读取文件的全部内容。 readFile 函数
这没有输出我们想要的结果。 这是因为我们没有在选项中提供编码; 要将内容读入字符串,我们需要提供编码选项。 这将我们的代码变成如下:
fs.readFile('index.js', 'utf-8', (error, data) => {
console.log(data);
});
现在,当我们运行前面的代码时,它会给我们如下输出:
图 9.35:使用 fs 读取文件的全部内容。 编码后的 readFile 函数
我们只是做了一个能输出自己的程序。
【中文翻译】 readFileSync(path, options)
这个函数做的事情与readFile
方法相同,但是同步执行read
函数,这意味着它将阻塞执行。 在程序启动期间,建议(也期望)只调用它一次。 当需要多次调用同步函数时,不建议使用它。
【中文翻译】 writeFile(文件,数据,选项,回调)
函数的作用是:将数据写入指定的文件。 它也将替换现有的文件,除非您传递一个附加为flag
选项。
fs.writeFileSync()
就像readFileSync
一样,它与它的非同步版本具有相同的功能。 它们之间的不同之处在于它同步地执行操作。
练习 69:Fs 模块的基本用法
在本练习中,我们将使用fs
模块在应用中读写文件。 我们将使用前一节中介绍的方法,并将它们与回调一起使用。 然后,我们将promisify
和async
、await
放在一起使用。
执行以下步骤来完成这个练习:
-
Create a new file called
test.txt
:js fs.writeFile('test.txt', 'Hello world', (error) => {
js if (error) {
js console.error(error);
js return;
js }
js console.log('Write complete');
js });
如果你做对了,你会看到下面的输出:
图 9.36:新建 test.txt 文件
你应该能够在与源代码相同的目录中看到新文件:
图 9.37:在与源代码相同的目录下创建的新文件
-
Read its contents and output it in the console:
js fs.readFile('test.txt', 'utf-8', (error, data) => {
js if (error) {
js console.error(error);
js }
js console.log(data);
js });
这只是简单地读取我们的文件; 我们提供编码是因为我们希望输出是一个字符串而不是一个缓冲区。 这将给我们以下输出:
图 9.38:使用 fs.readFile 读取文件内容
-
Try to read from a file that doesn't exist:
js fs.readFile('nofile.txt', 'utf-8', (error, data) => {
js if (error) {
js console.error(error);
js }
js console.log(data);
js });
当我们试图打开一个不存在的文件时,回调函数将被调用并返回错误。 建议在处理程序内部处理任何与文件相关的错误,而不是创建一个单独的函数来检查它。 当我们运行前面的代码时,我们会得到以下错误:
图 9.39:当我们试图读取一个不存在的文件时抛出的错误
-
Let's create our own version of
readFile
with promises:js function readFile(file, options) {
js return new Promise((resolve, reject) => {
js fs.readFile(file, options, (error, data) => {
js if (error) {
js return reject(error);
js }
js resolve(data);
js })
js })
js }
这和我们可以用任何基于回调的方法做的事情是一样的,如下所示:
js readFile('test.txt', 'utf-8').then(console.log);
这将生成以下输出:
图 9.40:使用基于回调的方法创建 readFile
-
Let's use file
stat
to get information about our file. After Node.js 10.0.0,fsPromises
was introduced, so instead of converting them into promises and returning functions manually, we can simply importfsPromise
and call the promised counterpart:js const fsPromises = require('fs').promises;
js fsPromises.stat('test.txt').then(console.log);
这将生成以下输出:
图 9.41:通过导入 fspromise 调用 promise 对应对象
在这里,您可以获得关于文件的大小、创建时间、修改时间和权限信息。
在这个练习中,我们复习了fs模块的一些基本用法。 它是 Node.js 中非常有用的模块。 接下来,我们将讨论如何在 Node.js 中处理大文件。
在 Node.js 中处理大文件
在前面的练习中,我们学习了如何使用fs
模块读取 Node.js 中的文件内容。 当我们处理小于 100mb 的小文件时,这工作得很好。当我们处理大文件(>2gb)时,有时,不可能使用fs.readFile
读取整个文件。 考虑以下场景。
您有一个 20gb 的文本文件,您需要逐行处理文件中的数据,并将输出写入输出文件。 你的电脑只有 8gb 的内存。
当您使用fs.readFile
时,它将尝试将文件的全部内容读入计算机的内存。 在我们的情况下,这是不可能的,因为我们的计算机没有安装足够的内存来容纳我们正在处理的文件的全部内容。 在这里,我们需要一个单独的方法来解决这个问题。 为了处理大文件,我们需要使用流。
流是编程中一个有趣的概念。 它不是把数据作为一个单独的内存块,而是一个每次从源数据块来的数据流。 这样,我们就不需要将所有数据放入内存中。 要创建文件流,我们只需使用fs
模块中提供的方法:
const fs = require('fs');
const stream = fs.createReadStream('file.txt', 'utf-8');
通过使用fs.createReadStream
,我们创建了一个文件流,稍后我们可以使用它来获取文件的内容。 我们像调用fs.readFile
一样调用这个函数,带有文件路径和编码。 与此不同的是,它不需要提供回调,因为它只是返回一个stream
对象。 为了从流中获取文件内容,我们需要将事件处理程序附加到stream
对象:
stream.on('data', data => {
// Data will be the content of our file
Console.log(data);
// Or
Data = data + data;
});
在data
事件的事件处理程序中,我们将获取文件的内容,当流读取文件时,这个处理程序将被多次调用。 当我们完成读取文件时,我们还会在 stream 对象上触发一个事件来处理这个事件:
stream.on('close', () => {
// Process clean up process
});
Util
Util是一个模块,包含了很多帮助 Node.js 内部 api 的函数。 这些对我们自己的开发也很有用。
util.callbackify(function)
当我们使用现有的基于回调的遗留代码处理async
和await
代码时,这是非常有用的。 要使用我们的async
函数作为基于回调的函数,我们可以调用util.callbackify
函数。 让我们考虑下面的例子:
async function outputSomething() {
return 'Something';
}
outputSomething().then(console.log);
下面是上述代码的输出:
图 9.42:使用 async 函数作为基于回调的函数
要使用这个async
函数和回调函数,只需调用callbackify
:
const callbackOutputSomething = util.callbackify(outputSomething);
然后,我们可以这样使用它:
callbackOutputSomething((err, result) => {
if (err) throw err;
console.log('got result', result);
})
这将生成以下输出:
图 9.43:通过调用 callbackify 函数来使用 async 函数
我们已经成功地将一个async
函数转换为使用回调的遗留函数。 当我们需要保持向后兼容性时,这是非常有用的。
功能:
在util模块中还有一个非常有用的方法来帮助我们promisify
基于回调的函数。 这个方法接受一个函数作为参数,并返回一个返回 promise 的新函数,如下所示:
function callbackFunction(param, callback) {
callback(null, 'I am calling back with: ${param}');
}
callbackFunction
接受一个参数,并将调用我们提供的带有新字符串的回调函数。 要将该函数转换为使用承诺,可以使用promisify
函数:
const promisifiedFunction = util.promisify(callbackFunction);
这将返回一个新函数。 稍后,我们可以使用它作为一个返回承诺的函数:
promisifiedFunction('hello world').then(console.log);
下面是上述代码的输出:
图 9.44:promise 函数用于回调
在util
模块中还有许多类型检查方法,当我们试图在应用中找出变量的类型时,这些方法非常有用。
【定时器 T0】
timer 模块为我们提供了一个用于调度计时器函数的 API。 我们可以使用它来设置部分代码的延迟,或者按所需的时间间隔执行代码。 与之前的模块不同,timer
模块在使用之前不需要导入。 让我们看看 Node.js 中提供的所有计时器函数,以及如何在应用中使用它们。
setInterval(callback, delay)
当我们想要设置一个被 Node.js 反复执行的函数时,我们可以使用setInterval
函数,同时提供一个回调和一个延迟。 要使用它,我们需要调用一个函数setInterval
,延迟以毫秒为单位。 例如,如果我们想每秒打印相同的消息,我们可以这样实现:
setInterval(() => {
console.log('I am running every second');
}, 1000);
当我们运行前面的代码时,我们将看到以下输出:
图 9.45:使用 setInterval 函数设置重复执行的函数
在这里,我们可以看到消息每秒钟都被打印出来。
setTimeout(callback, delay)
使用这个函数,我们可以设置函数的一次性延迟调用。 当我们想要在运行函数之前等待一定的时间时,我们可以使用setTimeout
来实现这一点。 在前面的小节中,我们还使用了setTimeout
来模拟测试中的网络和磁盘请求。 要使用它,我们需要传递一个想要运行的函数和一个以毫秒为单位的延迟整数。 如果我们想在 3 秒后打印消息,可以使用以下代码:
setTimeout(() => {
console.log('I waited 3 seconds to run');
}, 3000);
这将生成以下输出:
图 9.46:使用 setTimeout 函数设置函数的一次性延迟调用
您将看到消息在 3 秒后打印出来。 当我们需要延迟调用函数,或者只是想在测试中使用它来模拟 API 调用时,这是非常有用的。
setImmediate(callback)
通过使用此方法,我们可以在事件循环结束时执行一个函数。 如果您想在当前事件循环中运行完所有内容后调用某段代码,您可以使用setImmediate
来实现这一点。 看看下面的例子:
setImmediate(() => {
console.log('I will be printed out second');
});
console.log('I am printed out first');
在这里,我们创建了一个输出I will be printed out second
的函数,该函数将在事件循环结束时执行。 当我们执行这个时,我们会看到如下输出:
图 9.47:使用 setimime 推入的事件循环结束时要执行的函数
我们也可以用setTimeout
和0
作为延迟参数来达到同样的效果:
setTimeout(() => {
console.log('I will be printed out second');
}, 0);
console.log('I am printed out first');
clearInterval(timeout)
当我们使用setInterval
创建循环函数时,该函数也返回一个代表计时器的对象。 当我们想要停止 interval 运行时,我们可以使用clearInterval
来清除计时器:
const myInterval = setInterval(() => {
console.log('I am being printed out');
}, 1000);
clearInterval(myInterval);
当我们运行前面的代码时,我们将看不到输出,因为我们清除了刚刚创建的间隔,它没有机会运行:
图 9.48:使用 clearInterval 函数停止 interval 的运行
如果我们想要运行这个间隔 3 秒,我们可以将clearInterval
包装在setTimeout
内,这样它将在3.1
秒后清除我们的间隔。 我们给出了额外的 100 毫秒,因为我们希望在清空 interval 之前发生第三次调用:
setTimeout(() => {
clearInterval(myInterval);
}, 3100);
当我们运行前面的代码时,我们会看到输出输出 3 次:
图 9.49:使用 setTimeout 在指定的秒内对 clearInterval 进行包装
当我们处理多个预定的计时器时,这是非常有用的。 通过清除它们,我们可以避免应用中出现诸如内存泄漏和意外问题等问题。
活动 14:Building a File Watcher
在这个活动中,我们将使用计时器函数创建一个文件监视器,它将指示文件中的任何修改。 这些计时器函数将在文件上设置一个监视,并在每次文件发生变化时生成输出。 让我们开始:
- 我们需要创建一个
fileWatcher
类。 - 将创建一个文件监视器和一个要监视的文件。 如果没有文件存在,它将抛出异常。
- 文件监视器将采用另一个参数来存储检查之间的时间间隔。
- 文件监视器需要允许我们删除文件上的监视。
- 当文件更改时,文件监视程序需要发出文件更改事件。
- 当文件更改时,文件监视器将发出带有文件新内容的事件。
打开filewatcher.js
文件,并在该文件中做你的工作。 执行以下步骤来完成此活动:
- 进口我们的图书馆; 即
fs
和events
。 - 创建一个扩展了
EventEmitter
类的文件监视器类。 使用modify
时间戳跟踪文件更改。 - 创建
startWatch
方法来开始观察文件上的更改。 - 创建
stopWatch
方法以停止查看文件上的更改。 - 在与
filewatch.js
相同的目录下创建一个test.txt
文件。 - 创建一个
FileWatcher
实例并开始查看该文件。 - 修改
test.txt
中的部分内容并保存。 - 修改
startWatch
,以便它也检索新内容。 - 修改
startWatch
,以便当文件被修改时发出事件,当它遇到错误时发出错误。 - 将事件处理程序附加到错误并在
fileWatcher.
中更改它 -
Run the code and modify
test.txt
to see the result.请注意
这个活动的解决方案可以在 620 页找到。
如果您看到前面的输出,这意味着您的事件系统和文件读取工作正常。 请自行扩展此功能。 您还可以尝试查看整个文件夹或多个文件。 在这个活动中,我们只是使用文件系统模块和事件驱动编程创建了一个简单的fileWatcher
类。 使用这种方法可以帮助我们创建一个更小的代码库,并让我们在直接阅读代码时更加清晰。
小结
在本章中,我们讨论了 JavaScript 中的事件系统,以及如何使用内置的events
模块来创建我们自己的事件发射器。 稍后,我们讨论了一些有用的内置模块和它们的示例用法。 在编写需要多个组件相互通信的程序时,使用事件驱动编程可以帮助我们避免交错逻辑。 此外,通过使用内置模块,我们可以避免添加提供相同功能的模块,并避免创建具有巨大依赖性的项目。 我们还提到了如何使用计时器来控制程序执行,fs
来操作文件,path
来组合并获取关于文件路径的有用信息。 这些都是非常有用的模块,可以在以后构建应用时帮助我们。 在下一章中,我们将学习如何在 JavaScript 中使用函数式编程。
版权属于:月萌API www.moonapi.com,转载请注明出处