作者:Janith Kasun

翻译:疯狂的技术宅

原文:https://stackabuse.com/handli...

未经容许严禁转载

在本教程中咱们学习 Node.js 的原生 EvenEmitter 类。学完后你将理解事件、怎么应用 EvenEmitter 以及如何在程序中利用事件。另外还会学习 EventEmitter 类从其余本地模块扩大的内容,并通过一些例子理解背地的原理。

总之本文涵盖了对于 EventEmitter 类的所有内容。

什么是事件?

当今事件驱动的体系结构十分广泛,事件驱动的程序能够产生、检测和响应各种事件。

Node.js 的外围局部是事件驱动的,有许多诸如文件系统(fs)和 stream 这样的模块自身都是用 EventEmitter 编写的。

在事件驱动的编程中,事件(event) 是一个或多个动作的后果,这可能是用户的操作或者传感器的定时输入等。

咱们能够把事件驱动程序看作是公布-订阅模型,其中发布者触发事件,订阅者侦听事件并采取相应的措施。

例如,假如有一个服务器,用户能够向其上传图片。在事件驱动的编程中,诸如上传图片之类的动作将会收回一个事件,为了利用它,该事件还会有 1 到 n 个订阅者。

在触发上传事件后,订阅者能够通过向网站的管理员发电子邮件,让他们晓得用户已上传照片并对此做出反馈;另一个订阅者可能会收集无关操作的信息,并将其保留在数据库中。

这些事件通常是彼此独立的,只管它们也可能是相互依赖的。

什么是EventEmitter?

EventEmitter 类是 Node.js 的内置类,位于 events 模块。依据文档中的形容:

大部分的 Node.js 外围 API 都是基于习用的异步事件驱动的体系结构所实现的,在该体系结构中,某些类型的对象(称为“发射器”)收回已命名事件,这些事件会导致调用 Function 对象(“监听器”)”

这个类在某种程度上能够形容为公布-订阅模型的辅助工具的实现,因为它能够用简略的办法帮忙事件发送器(发布者)公布事件(音讯)给 监听器(订阅者)。

创立 EventEmitters

话虽如此,但还是先创立一个 EventEmitter 更加切实。能够通过创立类自身的实例或通过自定义类实现,而后再创立该类的实例来实现。

创立 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 秒

EventEmitter 与多个监听器

上面创立另一种事件发送器。这是一个计时程序,有三个侦听器。第一个监听器每秒更新一次工夫,第二个监听器在计时行将完结时触发,最初一个在计时完结时触发:

  • 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

接下来通过扩大 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秒后完结');});

EventEmitter 的次要函数

eventNames()

此函数将以数组模式返回所有流动的侦听器名称:

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', ...),则新事件也会增加到数组中。

这个办法不会返回已公布的事件,而是返回订阅的事件的列表。

removeListener()

这个函数能够从 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');

在第一个事件触发后,因为 f1f2 都处于活动状态,这两个函数都将被执行。之后从 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);

removeAllListeners()

该函数用于从 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() 会同时触发 f1f2,因为它们过后正处于活动状态。删除它们后,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 事件进行监听是一种很业余的做法。

应用 EventEmitter 的原生模块

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 写入规范输入管道时,将会触发 stdoutdata 事件。当输入流遇到谬误时,将从 stderr 管道发送 data 事件。

最初,在过程退出后,将会触发 close 事件。

总结

事件驱动的体系结构使咱们可能创立高内聚低耦合的零碎。事件示意某个动作的后果,能够定义 1个或多个侦听器并对其做出反馈。

本文深入探讨了 EventEmitter 类及其性能。对其进行实例化后间接应用,并将其行为扩大到了一个自定义对象中。

最初介绍了该类的一些重要函数。


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章

欢送持续浏览本专栏其它高赞文章:

  • 深刻了解Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13个帮你进步开发效率的古代CSS框架
  • 疾速上手BootstrapVue
  • JavaScript引擎是如何工作的?从调用栈到Promise你须要晓得的所有
  • WebSocket实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30分钟用Node.js构建一个API服务器
  • Javascript的对象拷贝
  • 程序员30岁前月薪达不到30K,该何去何从
  • 14个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把HTML转成PDF的4个计划及实现

  • 更多文章...