以事件为导向的编程是一个逻辑模式,我们可以选择将我们的编程限制在内,以避免复杂性和碰撞的问题. 在本文中,我们将讨论以事件为导向的编程是如何工作的,以及我们如何在 Node.js 项目中最大限度地利用它。
大多数开发人员早在他们对编程的研究中就熟悉了事件驱动编程的概念,但他们可能直到稍后才完全认识到这一点。
概览
对于任何级别的编程技能的人来说,事件驱动编程的最可识别的例子,我们将转向我们的老朋友Web浏览器。
每当您通过其用户界面与网页互动时,会发生一个事件. 当您点击按钮时,会触发一个点击事件.当您按下一个键时,会触发一个键下载事件.这些事件具有相关的函数,一旦触发,会执行以某种方式对用户界面进行更改。
以事件为导向的编程使用以下概念:
- 事件处理器是(https://developer.mozilla.org/en-US/docs/Glossary/Callback_function)在触发事件时会被调用的 [回调函数]
- 主循环听取事件触发器,并为该事件调用相关事件处理器
事件发行人
Node.js 自然为我们提供了一个名为 EventEmitter的有用的模块,它允许我们立即开始将事件驱动的编程纳入我们的项目。
两者都值得检查,如果您的项目需要运行比EventEmitter允许的更快。
我们通过事件
模块访问 EventEmitter 类,一旦导入,我们需要从类中创建一个新的对象,以便开始使用它。
1const EventEmitter = require('events').EventEmitter;
2const myEventEmitter = new EventEmitter;
现在我们可以开始在节点中使用事件驱动的编程。
想象一下,我们正在创建聊天室,我们希望在新用户加入聊天室时通知每个人,我们需要一个用户加入
事件的事件聆听器。
1const EventEmitter = require('events').EventEmitter;
2const chatRoomEvents = new EventEmitter;
3
4function userJoined(username){
5 // Assuming we already have a function to alert all users.
6 alertAllUsers('User ' + username + ' has joined the chat.');
7}
8
9// Run the userJoined function when a 'userJoined' event is triggered.
10chatRoomEvents.on('userJoined', userJoined);
下一步将是确保我们的聊天室在任何人登录时都会触发一个用户加入
事件,以便我们的事件处理器被调用。
1function login(username){
2 chatRoomEvents.emit('userJoined', username);
3}
我们可以通过创建事件来进一步扩展,当用户退出时,当发送消息时,当收到消息时,或任何其他事件,我们可能需要我们的聊天室像我们想要的那样动态。
删除听众
这可能是出于性能原因(该事件不再需要)或以避免记忆泄露(如果事件聆听器引用不再需要的对象,它将无法垃圾收集,这可能会导致不必要的对象的积累)。
要删除 EventEmitter 中的事件听众,我们可以使用删除Listener
或删除AllListeners
方法。重要的是要注意,在与 Node 结合的 EventEmitter 中,您必须通过使用删除Listener
方法删除您想要删除的确切函数的引用。
在下面的例子中,删除用户加入
函数之外的消息事件的收听者将是一个挑战,因为它是一个在关闭中宣布的匿名函数。在这种情况下,我们可以直接参考这种方法的唯一地方将是 EventEmitter 对象本身。
1const EventEmitter = require('events').EventEmitter;
2const chatRoomEvents = new EventEmitter;
3
4function userJoined(username){
5 chatRoomEvents.on('message', function(message){
6 document.write(message);
7 })
8}
9
10chatRoomEvents.on('userJoined', userJoined);
所有这些头痛都可以通过重写代码来避免:
1const EventEmitter = require('events').EventEmitter;
2const chatRoomEvents = new EventEmitter;
3
4function displayMessage(message){
5 document.write(message);
6}
7
8function userJoined(username){
9 chatRoomEvents.on('message', displayMessage);
10}
11
12chatRoomEvents.on('userJoined', userJoined);
现在,如果我们想从消息事件的处理器列表中删除displayMessage
函数:
1chatRoomEvents.removeListener('message', displayMessage);
对象导向编程 + 事件驱动编程
我想在这里触摸的最后一件事是对象导向和事件导向编程范式的结合,这两者在各种情况下构成了非常有价值的组合,我认为理解和概念化为什么会是有益的。
面向对象的方法促进了一个单个单位(或对象)的所有行为由该单位内的代码来处理的想法。
想象一下,我们正在构建一个邮件应用程序,我们可能有一个对象,其唯一的目的是为我们的客户端处理传入和传出邮件。这个对象将包含所有其自身的行为功能。我们可能有一个发送邮件
功能,将我们的邮件发送到服务器。
1const Mailbox = {
2 sendMail: function(){
3 // code to send mail.
4 },
5 receiveMail: function(){
6 // check server for new mail.
7 }
8}
现在,如果我们正在构建其他想要使用我们的 sendMail 函数的单位,他们可以通过Mailbox.sendMail
访问它,这是一个标准的方法,但是当我们后来决定我们也想每次发送邮件时登录邮件时会发生什么?我们现在需要修改我们的 sendMail 函数以包含这种行为,或者创建另一个函数,该函数在 sendMail 函数后立即启动。
这就是我们可以利用事件驱动的编程的地方。通过注册事件倾听者,我们实际上可以逆转我们的对象之间的通信流程。而不是对象需要到达另一个对象以触发函数,我们的对象只能发射事件,无论对象在听这些事件,都会以他们被告知的方式处理它。
让我们想象一下,我们有一个饥饿的鱼,由gator类代表,使用非事件驱动的方法,我们的鱼吃的过程看起来像这样:
1class Food {
2 constructor(name) {
3 this.name = name;
4 }
5
6 becomeEaten() {
7 return 'I have been eaten.';
8 }
9}
10
11var bacon = new Food('bacon');
12
13class gator {
14 eat() {
15 bacon.becomeEaten();
16 }
17}
在这个例子中,我们的gator必须访问食物内部的方法,以便吃。
1const EventEmitter = require('events').EventEmitter;
2const myGatorEvents = new EventEmitter;
3
4class Food {
5 constructor(name) {
6 this.name = name;
7 // Become eaten when gator emits 'gatorEat'
8 myGatorEvents.on('gatorEat', this.becomeEaten);
9 }
10
11 becomeEaten(){
12 return 'I have been eaten.';
13 }
14}
15
16var bacon = new Food('bacon');
17
18const gator = {
19 eat() {
20 myGatorEvents.emit('gatorEat');
21 }
22}
现在我们所有的gator只需要说gatorEat,EventEmitter会照顾其余的。