在 Node.js 中正确使用事件
在事件驱动编程流行之前,应用程序不同部分之间通信的标准方式非常简单:一个组件想要向另一个组件发送消息,需要显式调用该组件上的方法。但事件驱动的代码是为了做出反应而编写的,而不是为了被调用。
三项赛的益处
这种方法使我们的组件更加解耦。基本上,随着应用程序的不断编写,我们会识别事件,在正确的时间触发它们,并为每个事件附加一个或多个事件监听器。扩展功能变得更加容易,因为我们只需为特定事件添加更多监听器,而无需改动现有的监听器或触发该事件的应用程序部分。我们讨论的本质上就是观察者模式。
来源:https ://www.dofactory.com/javascript/observer-design-pattern
设计事件驱动架构
识别事件非常重要,因为我们不想最终不得不从系统中删除/替换现有事件,这可能会迫使我们删除/修改附加到该事件的任意数量的侦听器。我使用的一般原则是仅在业务逻辑单元完成执行时才考虑触发事件。
假设您想在用户注册后发送一堆不同的电子邮件。现在,注册过程本身可能涉及许多复杂的步骤,查询等。但从业务角度来看,它是一个步骤。并且要发送的每封电子邮件也是单独的步骤。因此,在注册完成后立即触发一个事件并附加多个侦听器是有意义的,每个侦听器负责发送一种类型的电子邮件。
Node 的异步事件驱动架构包含一些称为“发射器”的对象,它们会发出命名事件,从而调用名为“监听器”的函数。所有发出事件的对象都是EventEmitter类的实例。我们可以使用它来创建自己的事件。
一个例子
让我们使用内置事件模块(我鼓励您详细查看)来访问EventEmitter
。
// my_emitter.js
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
module.exports = myEmitter;
这是应用程序的一部分,我们的服务器接收 HTTP 请求,保存新用户并相应地发出事件:
// registration_handler.js
const myEmitter = require('./my_emitter');
// Perform the registration steps
// Pass the new user object as the message passed through by this event.
myEmitter.emit('user-registered', user);
我们在单独的模块中附加了一个监听器:
// listener.js
const myEmitter = require('./my_emitter');
myEmitter.on('user-registered', (user) => {
// Send an email or whatever.
});
将策略与实现分离是一种很好的做法。在这种情况下,策略意味着哪些监听器订阅了哪些事件,而实现则意味着监听器本身。
// subscriptions.js
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
// send_email_on_registration.js
module.exports = (user) => {
// Send a welcome email or whatever.
}
这种分离使得监听器可以重复使用,例如,它可以附加到发送相同消息(用户对象)的其他事件上。同样需要注意的是,当多个监听器附加到单个事件时,它们将同步执行,并按照附加的顺序执行。因此,监听器将在事件执行完成someOtherListener
后运行。 但是,如果您希望监听器异步运行,可以简单地像这样包装它们的实现:sendEmailOnRegistration
setImmediate
// send_email_on_registration.js
module.exports = (user) => {
setImmediate(() => {
// Send a welcome email or whatever.
});
}
保持听众清洁
编写监听器时,请遵循单一职责原则:一个监听器应该只做一件事,并且做好它。例如,避免在一个监听器中编写过多的条件语句,这些条件语句会根据事件传输的数据(消息)来决定执行哪些操作。在这种情况下,使用不同的事件会更合适:
// registration_handler.js
const myEmitter = require('./my_emitter');
// Perform the registration steps
// The application should react differently if the new user has been activated instantly.
if (user.activated) {
myEmitter.emit('user-registered:activated', user);
} else {
myEmitter.emit('user-registered', user);
}
// subscriptions.js
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
const doSomethingEntirelyDifferent = require('./do_something_entirely_different');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
myEmitter.on('user-registered:activated', doSomethingEntirelyDifferent);
必要时明确分离监听器
在上例中,我们的监听器是完全独立的函数。但如果监听器与某个对象关联(它是一个方法),则必须手动将其与其订阅的事件分离。否则,该对象将永远不会被垃圾回收,因为该对象的一部分(监听器)将继续被外部对象(事件发射器)引用。因此,存在内存泄漏的可能性。
例如,如果我们正在构建一个聊天应用程序,并且我们希望当用户连接到的聊天室中收到新消息时显示通知的责任应该由该用户对象本身承担,我们可能会这样做:
// chat_user.js
class ChatUser {
displayNewMessageNotification(newMessage) {
// Push an alert message or something.
}
// `chatroom` is an instance of EventEmitter.
connectToChatroom(chatroom) {
chatroom.on('message-received', this.displayNewMessageNotification);
}
disconnectFromChatroom(chatroom) {
chatroom.removeListener('message-received', this.displayNewMessageNotification);
}
}
当用户关闭标签页或暂时断网时,我们自然希望在服务器端触发一个回调,通知其他用户其中一人刚刚离线。当然,此时displayNewMessageNotification
对离线用户调用回调没有任何意义,但除非我们明确移除它,否则它会继续在收到新消息时被调用。如果不移除它,除了不必要的调用之外,用户对象还会无限期地停留在内存中。因此,请务必disconnectFromChatroom
在用户离线时调用服务器端回调。
提防
事件驱动架构中的松散耦合如果不谨慎处理,也会导致复杂性增加。追踪系统中的依赖关系(例如,哪些监听器最终会在哪些事件上执行)可能会很困难。如果我们开始从监听器内部发出事件,我们的应用程序就尤其容易出现这个问题,可能会触发一系列意外事件。
文章来源:https://dev.to/usamaashraf/using-events-in-nodejs-the-right-way-449b