热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

在Node.js中如何使用EventEmitter处理事件?

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

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

推荐教程:node js教程

总之本文涵盖了关于 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(&#39;error&#39;, new Error(&#39;countdownTimer 的值不能小于2&#39;));
        }

        this.countdownTime = countdownTime;
        this.currentTime = 0;
    }

    // ...........
}

处理这个错误的方式与其他事件相同:

myCountDown.on(&#39;error&#39;, (err) => {
    console.error(&#39;发生错误:&#39;, err);
});

始终对 error 事件进行监听是一种很专业的做法。

使用 EventEmitter 的原生模块

Node.js 中许多原生模块扩展了EventEmitter 类,因此它们本身就是事件发射器。

一个典型的例子是 Stream 类。官方文档指出:

流可以是可读的、可写的,或两者均可。所有流都是 EventEmitter 的实例。

先看一下经典的 Stream 用法:

const fs = require(&#39;fs&#39;);
const writer = fs.createWriteStream(&#39;example.txt&#39;);

for (let i = 0; i <100; i++) {
  writer.write(`hello, #${i}!\n`);
}

writer.on(&#39;finish&#39;, () => {
  console.log(&#39;All writes are now complete.&#39;);
});

writer.end(&#39;This is the end\n&#39;);

但是,在写操作和 writer.end() 调用之间,我们添加了一个侦听器。 Stream 在完成后会发出一个 finished 事件。在发生错误时会发出 error 事件,把读取流通过管道传输到写入流时会发出 pipe 事件,从写入流中取消管道传输时,会发出 unpipe 事件。

另一个类是 child_process 类及其 spawn() 方法:

const { spawn } = require(&#39;child_process&#39;);
const ls = spawn(&#39;ls&#39;, [&#39;-lh&#39;, &#39;/usr&#39;]);

ls.stdout.on(&#39;data&#39;, (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on(&#39;data&#39;, (data) => {
  console.error(`stderr: ${data}`);
});

ls.on(&#39;close&#39;, (code) => {
  console.log(`child process exited with code ${code}`);
});

child_process 写入标准输出管道时,将会触发 stdoutdata 事件。当输出流遇到错误时,将从 stderr 管道发送 data 事件。

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

总结

事件驱动的体系结构使我们能够创建高内聚低耦合的系统。事件表示某个动作的结果,可以定义 1个或多个侦听器并对其做出反应。

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

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

更多编程相关知识,请访问:编程课程!!

以上就是在Node.js中如何使用EventEmitter处理事件?的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • PHP设置MySQL字符集的方法及使用mysqli_set_charset函数
    本文介绍了PHP设置MySQL字符集的方法,详细介绍了使用mysqli_set_charset函数来规定与数据库服务器进行数据传送时要使用的字符集。通过示例代码演示了如何设置默认客户端字符集。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 一:跨域问题1、同源策略(浏览器的安全策略)    只允许当前页面朝当前域下发请求,如果向其他域发请求,请求可以正常发送,数据也可以拿回,但是被浏览器拦截了  2、c ... [详细]
  • 前言:原本纠结于Web 模板,选了Handlebars。后来发现页面都是弱逻辑的,不支持复杂逻辑表达式。几乎要放弃之际,想起了Javascript中eval函数。虽然eval函 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 本文详细介绍了解决全栈跨域问题的方法及步骤,包括添加权限、设置Access-Control-Allow-Origin、白名单等。通过这些操作,可以实现在不同服务器上的数据访问,并解决后台报错问题。同时,还提供了解决second页面访问数据的方法。 ... [详细]
author-avatar
他乡绿树_762
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有