作者:JanithKasun原生EvenEmitter类。完成后,您将了解事件、如何使用EvenEmitter以及如何在程序中使用事件。还要了解EventEmitter类从其他本机模块扩展的内容,并通过一些示例来理解其背后的基本原理。总之,本文涵盖了有关EventEmitter类的所有内容。什么是事件?事件驱动架构在今天非常普遍,事件驱动程序可以生成、检测和响应各种事件。Node.js的核心部分是事件驱动,文件系统(fs)、流等很多模块本身就是用EventEmitter写的。在事件驱动编程中,事件是一个或多个动作的结果,这些动作可能是用户动作,也可能是传感器的定时输出等。我们可以将事件驱动编程视为一种发布-订阅模型,发布者触发事件和订阅者听取它们并采取相应的行动。例如,想象一个用户可以上传图片的服务器。在事件驱动编程中,上传图片等动作会发出一个事件,该事件也会有1到n个订阅者才能使用它。触发上传事件后,订阅者可以通过向站点管理员发送电子邮件来对此作出反应,让他们知道用户已经上传了照片;另一个订阅者可能会收集有关该操作的信息并将其保存在数据库中。这些事件通常相互独立,但也可能相互依赖。什么是事件发射器?EventEmitter类是Node.js的内置类,位于events模块中。根据文档中的描述:大部分Node.js核心API都是基于惯用的异步事件驱动架构实现的,其中某些类型的对象(称为Namedevents导致Function对象(“监听器”)被调用”这个类在某种程度上可以被描述为发布-订阅模型的一个助手实现,因为它可以帮助事件发送者(publishPublisher)向监听者(订阅者)发布事件(消息)。话虽如此,它是首先创建一个EventEmitter更实用。这可以通过创建类本身的实例或通过实现自定义类然后创建该类的实例来完成。创建一个EventEmitter对象从一个简单的示例开始:创建一个EventEmitter,它每秒发出一个包含程序运行时信息的事件。首先从事件模块导入EventEmitter类:const{EventEmitter}=require('events');然后创建一个EventEmitter:consttimerEventEmitter=newEventEmitter();使用此对象发布事件非常容易:timerEventEmitter.emit("update");前面已经指定了事件名称,它将作为事件发布。但是程序中什么也没有发生,因为还没有监听器对这个事件做出反应。首先让这个事件每秒重复一次。使用setInterval()方法创建一个定时器,每秒发出一个更新事件:letcurrentTime=0;//每秒触发一个更新事件setInterval(()=>{currentTime++;timerEventEmitter.emit('update',currentTime);},1000);EventEmitter实例用于接受事件名称和参数。将update作为事件名称传递,将currentTime作为程序启动后的时间传递。发射器通过emit()方法触发,该方法使用我们提供的信息推送事件。事件发射器准备就绪后,订阅事件监听器:{time}秒`);});通过on()方法创建侦听器,传递事件名称以指定您希望侦听器附加到哪个事件。在更新事件上,运行记录时间的方法。on()函数的第二个参数是一个回调,可以接受事件发出的额外数据。运行代码会输出:Messagereceivedfrompublisher:programhasbeenrunningfor1secondMessagereceivedfrompublisher:programhasbeenrunningfor2secondsMessagereceivedfrompublisher:programhasbeenrunningfor1secondsMessagereceivedfrompublisher:programhasbeenrunningfor2secondsMessagereceivedfrompublisher:programhasbeenrunningfor3seconds..如果需要执行有些操作只有在事件第一次触发时才触发,也可以使用once()方法订阅:console.log(`程序已经运行${time}秒`);});运行这段代码会输出:Messagereceivedfromthepublisher:Theprogramhasbeenrunningfor1secondEventEmitterwithmultiple在监听器下方创建另一个事件发射器。这是一个带有三个监听器的定时器程序。第一个侦听器每秒更新一次时间,第二个侦听器在计时器即将结束时触发,最后一个在计时器结束时触发:update:每秒触发一次end:倒计时结束时触发end-soon:在Trigger2计时器结束前的秒数编写一个函数来创建此事件发射器:constcountDown=(countdownTime)=>{consteventEmitter=newEventEmitter();让当前时间=0;//每秒触发一次更新事件consttimer=setInterval(()=>{currentTime++;eventEmitter.emit('update',currentTime);//检查定时器是否结束if(currentTime===countdownTime){clearInterval(timer);eventEmitter.emit('end');}//检查定时器是否会在2秒后结束if(currentTime===countdownTime-2){eventEmitter.emit('end-soon');}},1000);返回事件发射器;};此函数启动一个事件,该事件每秒发出一个更新事件。第一个if检查计时器是否已超时并停止基于时间间隔的事件。如果结束,将发布结束事件。如果定时器还没有结束,那么检查定时器是否距离结束还有2秒,如果是,发布end-soon事件。向这个事件发射器添加一些订阅者:constmyCountDown=countDown(5);myCountDown.on('update',(t)=>{console.log(`程序已经运行${t}秒`);});myCountDown.on('end',()=>{console.log('Timerends');});myCountDown.on('end-soon',()=>{console.log('Timerwillendin2seconds');});这段代码会输出:程序已经运行了1秒程序已经运行了2秒程序已经运行了3秒定时器将在2秒后结束程序已经运行了4秒它已经运行了5秒,计时器结束。扩展EventEmitter接下来,通过扩展EventEmitter类来实现相同的功能。首先创建一个处理事件的CountDown类:const{EventEmitter}=require('events');classCountDownextendsEventEmitter{constructor(countdownTime){super();this.countdownTime=countdownTime;this.currentTime=0;}startTimer(){consttimer=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-很快');}},1000);您可以直接在类中使用this.emit()。另外,startTimer()函数用于控制计时开始的时间。否则会在创建对象后立即开始计时。创建一个新的CountDown对象并订阅它:constmyCountDown=newCountDown(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秒seconds定时器结束on()函数的别名是addListener()。看一下end-soon事件监听器:myCountDown.on('end-soon',()=>{console.log('计时将在2秒后结束');});也可以做与addListener()相同的操作,例如:myCountDown.addListener('end-soon',()=>{console.log('计时将在2秒后结束');});EventEmitter的主要函数eventNames()该函数将以数组的形式返回所有活动的侦听器名称:constmyCountDown=newCountDown(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');constemitter=newEventEmitter();constf1=()=>{console.log('f1被触发');}constf2=()=>{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都处于激活状态,所以这两个函数都会被执行。之后f1从EventEmitter中移除。当事件再次发出时,只会执行f2:f1isfiredf2isfiredf2isfiredremoveListener()的别名是off()。例如,我们可以这样写:removeListener()的别名是off()。比如可以这样写:emitter.off('some-event',f1);removeAllListeners()该函数用于从EventEmitter的所有事件中移除所有监听器:const{EventEmitter}=require('events');constemitter=newEventEmitter();constf1=()=>{console.log('f1istriggered');}constf2=()=>{console.log('f2istriggered');}emitter.on('some-event',f1);emitter.on('some-event',f2);emitter.emit('some-event');emitter.removeAllListeners();emitter.emit('some-event');第一个emit()同时触发f1和f2,因为它们当时处于活动状态。删除它们后,emit()函数发出事件,但没有侦听器响应它:f1isfiredf2isfired错误处理如果您想在EventEmitter中发出错误,则必须使用错误事件名称来完成。这是Node.js中所有EventEmitter对象的标准。该事件还必须有一个错误对象。例如,可以像这样发出错误事件:myEventEmitter.emit('error',newError('someerroroccurred'));错误事件的监听器应该有一个带参数的回调,用于捕获错误对象和过程。如果EventEmitter发出错误事件,但没有订阅者订阅错误事件,则Node.js程序将抛出此错误。这会导致Node.js进程停止运行并退出程序,并在控制台中显示错误的堆栈跟踪。比如CountDown类中,countdownTime参数的值不能小于2,否则不会触发end-soon事件。在这种情况下,应该发出错误事件:classCountDownextendsEventEmitter{constructor(countdownTime){super();if(countdownTimer<2){this.emit('error',newError('countdownTimer不能小于2'));}this.countdownTime=countdownTime;this.currentTime=0;}//.............}以与任何其他事件相同的方式处理此错误:myCountDown.on('error',(err)=>{console.error('Anerroroccurred:',呃);});始终监听错误事件是一种非常专业的做法。使用EventEmitter的原生模块Node.js中的许多原生模块都扩展了EventEmitter类,因此它们本身就是事件发射器。一个典型的例子是Stream类。官方文档指出:Streamscanbereadable,writable,orboth.所有流都是EventEmitter的实例。先看经典的Stream用法:constfs=require('fs');constwriter=fs.createWriteStream('example.txt');for(leti=0;i<100;i++){writer.write(`hello,#${i}!\n`);}writer.on('finish',()=>{console.log('所有写入现在都已完成。');});writer.end('到此结束\n');然而,在写入操作和调用writer.end()之间,我们添加了一个监听器。Stream在完成时发出一个完成的事件。error事件在发生错误时发出,pipe事件在读取流通过管道传输到写入流时发出,unpipe事件在管道从写入流中取消管道传输时发出。另一个类是child_process类及其spawn()方法:const{spawn}=require('child_process');constls=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(`子进程以代码${code}`退出);});当child_processwritestdout被管道传输时,stdout的数据事件将被触发。当输出流遇到错误时,会从stderr管道发送数据事件。最后,进程退出后,将触发关闭事件。总结事件驱动架构使我们能够创建高内聚和低耦合的系统。一个事件表示一个动作的结果,可以定义1个或多个侦听器并对其作出反应。本文深入介绍了EventEmitter类及其功能。实例化后直接使用,将其行为扩展到自定义对象中。最后介绍了该类的一些重要功能。
