做者:Janith Kasun翻译:疯狂的技术宅javascript
原文:https://stackabuse.com/handli...前端
未经容许严禁转载java
在本教程中咱们学习 Node.js 的原生 EvenEmitter
类。学完后你将了解事件、怎样使用 EvenEmitter
以及如何在程序中利用事件。另外还会学习 EventEmitter
类从其余本地模块扩展的内容,并经过一些例子了解背后的原理。node
总之本文涵盖了关于 EventEmitter
类的全部内容。程序员
当今事件驱动的体系结构很是广泛,事件驱动的程序能够产生、检测和响应各类事件。面试
Node.js 的核心部分是事件驱动的,有许多诸如文件系统(fs
)和 stream
这样的模块自己都是用 EventEmitter
编写的。数据库
在事件驱动的编程中,事件(event) 是一个或多个动做的结果,这多是用户的操做或者传感器的定时输出等。编程
咱们能够把事件驱动程序看做是发布-订阅模型,其中发布者触发事件,订阅者侦听事件并采起相应的措施。segmentfault
例如,假设有一个服务器,用户能够向其上传图片。在事件驱动的编程中,诸如上传图片之类的动做将会发出一个事件,为了利用它,该事件还会有 1 到 n 个订阅者。数组
在触发上传事件后,订阅者能够经过向网站的管理员发电子邮件,让他们知道用户已上传照片并对此作出反应;另外一个订阅者可能会收集有关操做的信息,并将其保存在数据库中。
这些事件一般是彼此独立的,尽管它们也多是相互依赖的。
EventEmitter
类是 Node.js 的内置类,位于 events
模块。根据文档中的描述:
大部分的 Node.js 核心 API 都是基于惯用的异步事件驱动的体系结构所实现的,在该体系结构中,某些类型的对象(称为“发射器”)发出已命名事件,这些事件会致使调用
Function
对象(“监听器”)”
这个类在某种程度上能够描述为发布-订阅模型的辅助工具的实现,由于它能够用简单的方法帮助事件发送器(发布者)发布事件(消息)给 监听器(订阅者)。
话虽如此,但仍是先建立一个 EventEmitter
更加实在。能够经过建立类自己的实例或经过自定义类实现,而后再建立该类的实例来完成。
先从一个简单的例子开始:建立一个 EventEmitter
,它每秒发出一个含有程序运行时间信息的事件。
首先从 events
模块中导入 EventEmitter
类:
const { EventEmitter } = require('events');
而后建立一个 EventEmitter
:
const timerEventEmitter = new EventEmitter();
用这个对象发布事件很是容易:
timerEventEmitter.emit("update");
前面已经指定了事件名,并把它发布为事件。可是程序没有任何反应,由于尚未侦听器对这个事件作出反应。
先让这个事件每秒重复一次。用 setInterval()
方法建立一个计时器,每秒发布一次 update
事件:
let currentTime = 0; // 每秒触发一次 update 事件 setInterval(() => { currentTime++; timerEventEmitter.emit('update', currentTime); }, 1000);
EventEmitter
实例用来接受事件名称和参数。把 update
做为事件名, currentTime
做为自程序启动以来的时间进行传递。
经过 emit()
方法触发发射器,该方法用咱们提供的信息推送事件。准备好事件发射器以后,为其订阅事件监听器:
timerEventEmitter.on('update', (time) => { console.log('从发布者收到的消息:'); console.log(`程序已经运行了 ${time} 秒`); });
经过 on()
方法建立侦听器,并传递事件名称来指定但愿将侦听器附加到哪一个事件上。 在 update
事件上,运行一个记录时间的方法。
on()
函数的第二个参数是一个回调,能够接受事件发出的附加数据。
运行代码将会输出:
从发布者收到的消息: 程序已经运行了 1 秒 从发布者收到的消息: 程序已经运行了 2 秒 从发布者收到的消息: 程序已经运行了 3 秒 ...
若是只在事件首次触发时才须要执行某些操做,也能够用 once()
方法进行订阅:
timerEventEmitter.once('update', (time) => { console.log('从发布者收到的消息:'); console.log(`程序已经运行了 ${time} 秒`); });
运行这段代码会输出:
从发布者收到的消息: 程序已经运行了 1 秒
下面建立另外一种事件发送器。这是一个计时程序,有三个侦听器。第一个监听器每秒更新一次时间,第二个监听器在计时即将结束时触发,最后一个在计时结束时触发:
update
:每秒触发一次end
:在倒数计时结束时触发end-soon
:在计时结束前 2 秒触发先写一个建立这个事件发射器的函数:
const countDown = (countdownTime) => { const eventEmitter = new EventEmitter(); let currentTime = 0; // 每秒触发一次 update 事件 const timer = setInterval(() => { currentTime++; eventEmitter.emit('update', currentTime); // 检查计时是否已经结束 if (currentTime === countdownTime) { clearInterval(timer); eventEmitter.emit('end'); } // 检查计时是否会在 2 秒后结束 if (currentTime === countdownTime - 2) { eventEmitter.emit('end-soon'); } }, 1000); return eventEmitter; };
这个函数启动了一个每秒钟发出一次 update
事件的事件。
第一个 if
用来检查计时是否已经结束并中止基于间隔的事件。若是已结束将会发布 end
事件。
若是计时没有结束,那么就检查计时是否是离结束还有 2 秒,若是是则发布 end-soon
事件。
向该事件发射器添加一些订阅者:
const myCountDown = countDown(5); myCountDown.on('update', (t) => { console.log(`程序已经运行了 ${t} 秒`); }); myCountDown.on('end', () => { console.log('计时结束'); }); myCountDown.on('end-soon', () => { console.log('计时将在2秒后结束'); });
这段代码将会输出:
程序已经运行了 1 秒 程序已经运行了 2 秒 程序已经运行了 3 秒 计时将在2秒后结束 程序已经运行了 4 秒 程序已经运行了 5 秒 计时结束
接下来经过扩展 EventEmitter
类来实现相同的功能。首先建立一个处理事件的 CountDown
类:
const { EventEmitter } = require('events'); class CountDown extends EventEmitter { constructor(countdownTime) { super(); this.countdownTime = countdownTime; this.currentTime = 0; } startTimer() { const timer = setInterval(() => { this.currentTime++; this.emit('update', this.currentTime); // 检查计时是否已经结束 if (this.currentTime === this.countdownTime) { clearInterval(timer); this.emit('end'); } // 检查计时是否会在 2 秒后结束 if (this.currentTime === this.countdownTime - 2) { this.emit('end-soon'); } }, 1000); } }
能够在类的内部直接使用 this.emit()
。另外 startTimer()
函数用于控制计时开始的时间。不然它将在建立对象后当即开始计时。
建立一个 CountDown
的新对象并订阅它:
const myCountDown = new CountDown(5); myCountDown.on('update', (t) => { console.log(`计时开始了 ${t} 秒`); }); myCountDown.on('end', () => { console.log('计时结束'); }); myCountDown.on('end-soon', () => { console.log('计时将在2秒后结束'); }); myCountDown.startTimer();
运行程序会输出:
程序已经运行了 1 秒 程序已经运行了 2 秒 程序已经运行了 3 秒 计时将在2秒后结束 程序已经运行了 4 秒 程序已经运行了 5 秒 计时结束
on()
函数的别名是 addListener()
。看一下 end-soon
事件监听器:
myCountDown.on('end-soon', () => { console.log('计时将在2秒后结束'); });
也能够用 addListener()
来完成相同的操做,例如:
myCountDown.addListener('end-soon', () => { console.log('计时将在2秒后结束'); });
此函数将以数组形式返回全部活动的侦听器名称:
const myCountDown = new CountDown(5); myCountDown.on('update', (t) => { console.log(`程序已经运行了 ${t} 秒`); }); myCountDown.on('end', () => { console.log('计时结束'); }); myCountDown.on('end-soon', () => { console.log('计时将在2秒后结束'); }); console.log(myCountDown.eventNames());
运行这段代码会输出:
[ 'update', 'end', 'end-soon' ]
若是要订阅另外一个事件,例如 myCount.on('some-event', ...)
,则新事件也会添加到数组中。
这个方法不会返回已发布的事件,而是返回订阅的事件的列表。
这个函数能够从 EventEmitter
中删除已订阅的监听器:
const { EventEmitter } = require('events'); const emitter = new EventEmitter(); const f1 = () => { console.log('f1 被触发'); } const f2 = () => { console.log('f2 被触发'); } emitter.on('some-event', f1); emitter.on('some-event', f2); emitter.emit('some-event'); emitter.removeListener('some-event', f1); emitter.emit('some-event');
在第一个事件触发后,因为 f1
和 f2
都处于活动状态,这两个函数都将被执行。以后从 EventEmitter
中删除了 f1
。当再次发出事件时,将会只执行 f2
:
f1 被触发 f2 被触发 f2 被触发
An alias for removeListener()
is off()
. For example, we could have written:
removeListener()
的别名是 off()
。例如能够这样写:
emitter.off('some-event', f1);
该函数用于从 EventEmitter
的全部事件中删除全部侦听器:
const { EventEmitter } = require('events'); const emitter = new EventEmitter(); const f1 = () => { console.log('f1 被触发'); } const f2 = () => { console.log('f2 被触发'); } emitter.on('some-event', f1); emitter.on('some-event', f2); emitter.emit('some-event'); emitter.removeAllListeners(); emitter.emit('some-event');
第一个 emit()
会同时触发 f1
和 f2
,由于它们当时正处于活动状态。删除它们后,emit()
函数将发出事件,但没有侦听器对此做出响应:
f1 被触发 f2 被触发
若是要在 EventEmitter
发出错误,必须用 error
事件名来完成。这是 Node.js 中全部 EventEmitter
对象的标准配置。这个事件必须还要有一个 Error
对象。例如能够像这样发出错误事件:
myEventEmitter.emit('error', new Error('出现了一些错误'));
error
事件的侦听器都应该有一个带有一个参数的回调,用来捕获 Error
对象并处理。若是 EventEmitter
发出了 error
事件,可是没有订阅者订阅 error
事件,那么 Node.js 程序将会抛出这个 Error
。这会致使 Node.js 进程中止运行并退出程序,同时在控制台中显示这个错误的跟踪栈。
例如在 CountDown
类中,countdownTime
参数的值不能小于 2,不然会没法触发 end-soon
事件。在这种状况下应该发出一个 error
事件:
class CountDown extends EventEmitter { constructor(countdownTime) { super(); if (countdownTimer < 2) { this.emit('error', new Error('countdownTimer 的值不能小于2')); } this.countdownTime = countdownTime; this.currentTime = 0; } // ........... }
处理这个错误的方式与其余事件相同:
myCountDown.on('error', (err) => { console.error('发生错误:', err); });
始终对 error
事件进行监听是一种很专业的作法。
Node.js 中许多原生模块扩展了EventEmitter
类,所以它们自己就是事件发射器。
一个典型的例子是 Stream
类。官方文档指出:
流能够是可读的、可写的,或二者都可。全部流都是
EventEmitter
的实例。
先看一下经典的 Stream 用法:
const fs = require('fs'); const writer = fs.createWriteStream('example.txt'); for (let i = 0; i < 100; i++) { writer.write(`hello, #${i}!\n`); } writer.on('finish', () => { console.log('All writes are now complete.'); }); writer.end('This is the end\n');
可是,在写操做和 writer.end()
调用之间,咱们添加了一个侦听器。 Stream
在完成后会发出一个 finished
事件。在发生错误时会发出 error
事件,把读取流经过管道传输到写入流时会发出 pipe
事件,从写入流中取消管道传输时,会发出 unpipe
事件。
另外一个类是 child_process
类及其 spawn()
方法:
const { spawn } = require('child_process'); const ls = spawn('ls', ['-lh', '/usr']); ls.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); ls.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); ls.on('close', (code) => { console.log(`child process exited with code ${code}`); });
当 child_process
写入标准输出管道时,将会触发 stdout
的 data
事件。当输出流遇到错误时,将从 stderr
管道发送 data
事件。
最后,在进程退出后,将会触发 close
事件。
事件驱动的体系结构使咱们可以建立高内聚低耦合的系统。事件表示某个动做的结果,能够定义 1个或多个侦听器并对其作出反应。
本文深刻探讨了 EventEmitter
类及其功能。对其进行实例化后直接使用,并将其行为扩展到了一个自定义对象中。
最后介绍了该类的一些重要函数。