当前位置: 首页 > 后端技术 > Node.js

结识node核心模块--深入EventEmitter

时间:2023-04-04 00:07:13 Node.js

原文地址在我的博客,转载请注明出处,谢谢!Node采用事件驱动机制,EventEmitter是Node实现事件驱动的基础。在EventEmitter的基础上,node的几乎所有模块都继承了这个类,实现了异步事件驱动的架构。继承自EventEmitter的模块有自己的事件,可以绑定/触发监听器,实现异步操作。EventEmitter是节点事件模型的基础。建立在EventEmitter基础上的事件驱动架构处处体现着异步编程的思想。所以我们在构建节点程序的时候一定要遵循这个思路。EventEmitter的实现原理是观察者模式,也是实现事件驱动的基本模式。本文将重点介绍EventEmitter,从中探讨其观察者模式的原理,体现异步编程的思想和应用。文本事件模块的EventEmitter类。node的事件模块只提供了一个EventEmitter类。该类实现了节点异步事件驱动架构的基本模式——观察者模式,并提供绑定事件、触发事件等事件监听模式。提供的API:constEventEmitter=require('events')classMyEmitterextendsEventEmitter{}constmyEmitter=newMyEmitter()functioncallback(){console.log('eventeventtriggered!')}myEmitter.on('event',回调)myEmitter.emit('事件')myEmitter.removeListener('事件',回调);只要继承EventEmitter类,就可以有事件,触发事件等,所有可以触发事件的对象都是EventEmitter类的实例。观察者模式(事件发布/订阅模式)是实现EventEmitter类的基本原则,也是事件驱动机制的基本模式。事件驱动原理:观察者模式如何在事件驱动系统中产生事件?为什么一个事件可以发生“自动”调用回调函数?我们先来看观察者模式。观察者(Observer)模式是一种设计模式,应用场景是当一个对象的变化需要通知其他多个对象并且这些对象需要松耦合时。在这种模式下,被观察对象(subject)维护着一组由其他对象发送(注册)的观察者。如果一个新的对象对该主题感兴趣,它会注册观察者,如果不感兴趣,它会取消订阅。如果主题更新,观察者将依次收到通知。说到ape:functionSubject(){this.listeners={}}Subject.prototype={//添加事件监听器addListener:function(eventName,callback){if(typeofcallback!=='function')thrownewTypeError('"listener"参数必须是一个函数')if(typeofthis.listeners[eventName]==='undefined'){this.listeners[eventName]=[]}this.listeners[eventName].push(callback)//放在观察者对象中},//取消监听一个回调removeListener:function(eventName,callback){if(typeofcallback!=='function')thrownewTypeError('"listener"argumentmustbeafunction')if(Array.isArray(this.listeners[eventName])&&this.listeners[eventName].length!==0){varcallbackList=this.listeners[eventName]for(vari=0,len=callbackList.length;ievent.triggerEvent('hello'),1000)//一秒钟后,观察时什么都不输出在观察者模式下,注册的回调函数就是事件监听器,触发事件调用各个回调函数就是发布消息。可以看到观察者模式只维护了一个信号对应的函数列表,可以存储和删除。你只需要给它信号(index),它根据信号执行相应的函数,相当于间接调用。那直接调用函数就可以了,干嘛写的这么斜?刚才说了,这是因为观察者模式可以解耦对象之间的关系,实现表现层和数据逻辑层的分离,定义稳定的更新消息传递机制。回到最初的问题,事件是如何“自动”产生和调用的?是不是像上面那样调用event.triggerEvent时产生的?不是,调用event.triggerEvent相当于调用了回调函数,是事件执行过程,而事件产生过程更多是底层产生并通知给node。我们以node的全局变量process为例,process是EventEmitter的一个实例:process.on('exit',(code)=>{console.log(`Abouttoexitwithcode:${code}`);});node执行时,会在进程的退出事件上绑定你指定的回调,相当于调用上面的addListener。当你退出进程的时候,你会发现你指定的函数已经执行了,但是你并没有手动调用trigger退出事件的方法,也就是上面的triggerEvent,是因为节点底层调用了它you——操作系统底层让进程退出,node会得到这个信息,然后触发预定义的触发方法,回调函数就这样依次执行。像这样的内置事件是由节点模块预先编写和打开的。使用时,直接绑定回调函数即可。如果要自定义事件,则必须自己发送信号。上面的代码实现了最基本的观察者模式。节点源码中EventEmitter的实现原理与此类似。除了这些,还增加了其他有用的特性,各种实现都尽可能使用性能最好的方法(node源码真是无处不在,体现智慧之光)。node中很多模块都继承了EventEmitter,比如文件模块系统下的FSWatcher:constEventEmitter=require('events')constutil=require('util')...functionFSWatcher(){EventEmitter.call(this);//调用构造函数...}util.inherits(FSWatcher,EventEmitter);//其他继承EventEmitter的模块也是如此。它们共同构成了节点的异步事件驱动架构。异步编程范式可以看出由于使用了事件模型和异步I/O,node中大量模块的API采用了异步回调函数的方式,底层也体现了异步编程的方式到处。虽然异步也带来了很多问题——理解困难、回调嵌套较深、错误捕获困难、多线程编程困难等等,但是相对于异步带来的高性能,这些问题是更好的解决方案,异步编程paradigm还是值得尝试的,尤其是在使用node构建应用的时候。从最基本的回调函数说起,回调函数是异步编程的体现,回调函数的实现离不开高阶函数。得益于javascript语言的灵活性,将函数作为参数或返回值,将函数作为参数或返回值的函数是高阶函数:functionfoo(x,bar){returnbar(x)}//对于同一个foo,不同的bar会有不同的运算结果vararr=[2,3,4,5]arr.forEach(function(item,index){//对每个item做一些事情})//heightofthearrayOrderfunctionevent.addListener('hello',hello)//还有上面观察者模式实现的addListener,可以根据高阶函数的特点实现回调函数模式。事实上,正是因为JavaScript的函数非常灵活,才有了高阶函数和众多的设计模式。采用事件发布/订阅模式(观察者模式),简单地使用高阶函数特性,不足以构建简单、灵活、强大的异步编程模式应用程序,我们需要借鉴其他语言的一些设计模式。如上所述,Node的事件模块实现了事件发布/订阅模式,这是一种在异步编程中广泛使用的模式。它将回调函数变成事件,并将事件与每个回调函数相关联。注册回调函数意味着添加事件监听器。这些事件监听器可以很容易地添加、删除和执行,从而使事件和处理逻辑(注册回调函数)之间轻松实现关联和解耦——事件发布者不需要关注监听器是如何实现业务逻辑的,也不需要关注有多少个事件监听器,只需要根据消息执行即可,数据通过该方法可以灵活传输。不仅如此,这种模式还可以像类一样实现对功能的封装:把不变的逻辑封装在里面,把需要定制和容易改变的部分通过事件暴露给外部定义。Node中的大部分对象都具有这种黑盒的特征。通过事件钩子,用户不需要关注对象是如何启动的,而只需要关注自己关心的事件。和大多数node核心模块一样,通过继承EventEmitter,我们可以使用这种模式来帮助我们在异步编程中构建node程序。使用PromisePromise是CommonJs发布的规范,它的出现给异步编程带来了方便。Promise所做的是封装异步调用和嵌套回调,让嵌套逻辑复杂不清晰的回调变得优雅易懂。使用Promise封装,您可以像这样编写异步调用:functionfn1(resolve,reject){setTimeout(function(){console.log('Step1:Execute');resolve('1');},500);}functionfn2(resolve,reject){setTimeout(function(){console.log('Step2:Execute');resolve('2');},100);}newPromise(fn1).then(function(val){console.log(val);returnnewPromise(fn2);}).then(function(val){console.log(val);return33;}).then(function(val){console.log(val);});Promise是怎么封装的?首先,Promise经常被用来处理异步和延迟操作。为了以正确的顺序执行then中的“下一步要做的事情”,Promise被设计成一个状态机,状态变化是pending=>resolve(成功),pending=>reject(失败),并且,Promise还维护在成功或失败时要执行的函数列表。List中的回调正是Promise处于pending状态时在then中注册的回调;Promise内部有resolve和reject函数,分别在成功/失败时执行函数List,这两个函数会传递给回调函数,由用户决定何时resolve/reject;为了实现链式调用,then中返回的是promise:functiongetUserId(){returnnewPromise(function(resolve,i){//异步请求setTimeout(function(){console.log('异步操作成功,下一步就是执行promise的'+i+'的resolve')resolve('FuckyouPromise!',i)},1000)},1)}getUserId().then(function(words){console.log(words)})//实现functionPromise(fn,i){vari=ivarstate='pending'varresult=nullvarpromises=[]console.log('Promise'+i+'constructing')this.then=function(onFulfilled){console.log('then被调用')returnnewPromise(function(resolve){console.log('返回一个promise')handle({onFulfilled:onFulfilled||null,resolve:function(ret,i){resolve(ret,i)}})},2)}functionhandle(promise){if(state==='pending'){console.log('promise'+i+'还在pending中')promises.push(promise)console.log('注册回调')return}if(!promise.onFulfilled){console.log('回调为空,resolve结果')promise.resolve(result,i)return}console.log('执行回调')varret=promise.onFulfilled(result)console.log('处理回调返回的值(可能是另一个promise)')promise.resolve(ret,2)}functionresolve(newResult,i){console.log('executepromise'+i+'resolve')if(newResult&&(typeofnewResult==='object'||typeofnewResult==='function')){console.log('thenregisteredcallbackreturnedpromise')varthen=newResult.thenif(typeofthen==='function'){console.log('callthen')then.call(newResult,resolve)}}console.log('设置promise的状态'+i+'isfulfilled')state='fulfilled'result=newResultsetTimeout(function(){console.log('Traversepromise'+i+'registeredcallbackexecution')console.log(promises[0])promises.forEach(function(promise){handle(promise)});},0)}console.log('passresolvetopromise'+i+'functionparameter')fn(resolve,i)}注意这是Promise/A+规范的简单实现,拒绝的原理是一样的。为了更好的理解promise,避免混淆,我添加了标签,方便理解。在Promise/A+规范中根本没有。事实上,高版本的node已经支持promise,可以直接使用,但是速度不如Bluebird等第三方库,而且Bluebird扩展了很多Promise/A+没有的方法。使用第三方库Async/Stepasync是一个知名的流程控制库,经常被npm安装,它提供了20多个方法来帮助我们处理异步协作模式。例如:series——异步任务的串行执行,就像Promise一样,只是形式不同parallel——异步任务的并行执行,相当于Promise.allwaterfall——处理带有依赖的异步调用,比如前面的结果就是后面的一个输入auto——自动分析异步调用的依赖关系。参数是一个依赖对象……Step比async更轻、更简单。Step只有一个接口,在该接口中调用Step提供的方法即可。功能和async差不多。异步编程范例远不止于此。有很多重要的思想和设计模式,有些需要在实践中去发现和总结。总结EventEmitter提供的接口非常简单,但其背后的思想却贯穿于Node的整个架构。Node并不是第一个使用异步编程的平台,但是异步架构在Node中无处不在,这是Node设计的基本思想。学习节点时,透过现象看本质,深入浅出是一个明智的做法,凡事皆如此。参考资料:https://segmentfault.com/a/11...【蒲玲】《深入浅出Node.Js》