宝哥软件园

理解JavaScript事件发射器

编辑:宝哥软件园 来源:互联网 时间:2021-08-31

两个多月前我在Github上复制了eventemitter3的源代码,在Node.js下复制了events模块,终于了解了JavaScript事件。

上周末,根据我之前对源代码的理解,我花了一些时间用ES6实现了一个eventemitter8,然后发布给npm。让我惊讶的是,有45次下载没有自述介绍,也没有任何宣传。我很好奇是谁下载的,能不能用。我花了很多时间复制了一个半原始的JavaScript时间处理库now.js (npm portal: now.js)。在我的大力宣传下,4个月的下载量只有177。种花种花真的很难,但不是故意的!

eventemitter8的大部分内容都是我看完源代码后写的。有些方法,比如listeners、listenerCount和eventNames,都记不清具体做什么了,我稍后会重新检查。许多测试案例都引用了eventemitter3。感谢eventemitter3的开发者和Node.js事件模块的开发者。

让我们谈谈我对JavaScript事件的理解:

从上图可以看出,JavaScript事件的核心包括事件监听器、事件触发器和事件removeListener。

事件监听(添加监听程序)

首先,倾听必须有一个倾听的目标,或者说一个对象。为了区分目标,名称是必不可少的,我们将其定义为类型。

其次,监听的目标必须有某种动作,对应JavaScript中的某个方法,这里定义为fn。

例如,你可以听一个事件,它的类型是add,它的方法是方法fn=()=a 1,它将变量a的值加1,如果我们想听一个将变量b加2的方法,我们的第一反应可能是创建一个类型为add2,方法fn1=()=b 2的事件。你可能会想,这是浪费。我可以只听一个名字,让它执行多个方法的事件吗?当然有可能。

那么怎么做呢?

很简单,把监控方法放在一个数组中,遍历数组依次执行。上面的例子改为类型add,方法为[fn,fn1]。

如果要对其进行细分,还可以将其分为上的一个可以无限期执行的事件和上的一个只能执行一次的事件(执行后立即删除该事件)。细节将在稍后给出。

事件触发(发射)

光有事件监控是不够的,整个过程只有由事件触发才能完成。Emit是对应于特定类型的单个事件或一系列事件,用于触发监控。以前面的例子为例,单个事件执行fn,一系列事件遍历fn和fn1。

事件移除(移除监听器)

严格来说,事件监控和事件触发可以完成整个过程。事件删除是可选的。但是在很多情况下,我们仍然需要删除事件。例如,一次事件只允许执行一次。如果不提供删除方法,很难保证什么时候会再次执行。通常只要不再需要,就应该删除。

核心部分完成后,下面简单分析一下eventemitter8的源代码。

源代码解析

所有源代码:

const ToString=对象。原型。ToStringconst isType=obj=ToString。打电话.切片(8,-1)。toLowerCase();const isArray=obj=array。isArray(obj)| | isType(obj)===' array ';const isNullOrUndefined=obj=obj===null | | obj===未定义;const _ addListener=function(type,fn,context,once) { if (typeof fn!==' function '){ 0引发新的TypeError('fn必须是函数');} fn.context=contextfn.once=!一次;常数事件=这个_ events[type];//只有一个,让这个_events[type]`成为函数if(isNullOrUndefined(event)){ this ._ events[type]=fn;} else if(事件类型==' function '){//已经有一个函数,`这个._事件[类型]`必须是此之前的函数_events[type]=[event,fn];} else if (isArray(event)) { //已经有多个函数,只需推送这个即可。_事件[类型]。push(fn);}退回这个;};类事件发射器{构造函数(){ if(this ._events===undefined) { this ._事件=对象。创建(空);} } addListener(类型,fn,上下文){ return _ addListener。调用(this、type、fn、context);{ on(type,fn,context){ return this。addlistener(类型、fn、上下文);}一次(类型,fn,上下文){ return _ addlistener。调用(this,type,fn,context,true);}发出(类型,rest){ if(isNullOrUndefined(type)){ 0抛出新的错误('发出必须接收至少一个参数');}常量事件=这个_ events[type];if (isNullOrUndefined(事件))返回falseif(事件类型==' function '){ events。调用(事件。上下文| | null,rest);if(事件。曾经){这个。removelistener(类型,事件);} } else if(IsArray(events)){ events。map(e={ e . call(e . context | | null,rest);如果(例如一次){这个。removelistener(类型,e);} });}返回true} removeListener(类型,fn) { if (isNullOrUndefined)(这事件)返回这个;//如果类型未定义或者为null,什么都不做,只返回这个if (isNullOrUndefined(类型))返回这个;if(fn的类型!==' function '){ 0引发新错误(' fn必须是函数');}常量事件=这个_ events[type];if(事件类型==' function '){事件===fn删除此_ events[type];} else { const find index=事件。find index(e=e===fn);if (findIndex===-1)返回这个;//匹配第一个,移位比拼接快if(find index===0){ events。shift();} else { events.splice(findIndex,1);} //只留下一个侦听器,如果(events.length===1) {这个,将排列更改为功能._ events[type]=events[0];} }返回此;} removeAllListeners(类型){ if (isNullOrUndefined(这事件)返回这个;//如果不提供类型,则移除所有if (isNullOrUndefined(类型))这个事件=对象。创建(空);常量事件=这个_ events[type];if(!isNullOrUndefined(events)) { //检查“类型”是否是最后一个if (Object.keys(这。_事件)。length===1) { this ._事件=对象。创建(空);} else {删除此内容_ events[type];} }返回此;}侦听器(类型){ if (isNullOrUndefined(这事件)返回[];常量事件=这个_ events[type];//使用地图',因为我们需要返回一个新的数组返回isNullOrUndefined(事件)?[] :(事件类型=='函数'?[事件]:事件。map(o=o));} listenerCount(类型){ if (isNullOrUndefined(此事件)返回0;常量事件=这个_ events[type];返回isNullOrUndefined(事件)?0 :(事件类型==='函数'?1 :事件。长度);}事件名称(){ if(isNullOrUndefined)(这事件)返回[];返回Object.keys(这个。_事件);} }导出默认事件发射器;代码很少,只有151行,因为写的简单版,且用的ES6,所以才这么少;Node.js的事件和eventemitter3可比这多且复杂不少,有兴趣可自行深入研究。

const ToString=对象。原型。ToStringconst isType=obj=ToString。打电话.切片(8,-1)。toLowerCase();const isArray=obj=array。isArray(obj)| | isType(obj)===' array ';const isNullOrUndefined=obj=obj===null | | obj===未定义;这四行就是一些工具函数,判断所属类型、判断是否是空或者未定义。

构造函数(){ if (isNullOrUndefined)(这_ events){ }这事件=对象。创建(空);}}创建了一个事件发射器类,然后在构造函数里初始化一个类的_事件属性,这个属性不需要要继承任何东西,所以用了对象。创建(空).当然这里isNullOrUndefined(这个。_事件)还去判断了一下这个。_事件是否为不明确的或者null,如果是才需要创建。但这不是必要的,因为实例化一个事件发射器都会调用构造函数,皆为初始状态,这个。_事件应该是不可能已经定义了的,可去掉。

addListener(类型,fn,上下文){ return _ addListener。调用(this、type、fn、context);{ on(type,fn,context){ return this。addlistener(类型、fn、上下文);}一次(类型,fn,上下文){ return _ addlistener。调用(this,type,fn,context,true);}接下来是三个方法添加监听器,打开,一次,其中在是添加侦听器的别名,可执行多次一次。只能执行一次。

三个方法都用到了_addListener方法:

const _ addListener=function(type,fn,context,once) { if (typeof fn!==' function '){ 0引发新的TypeError('fn必须是函数');} fn.context=contextfn.once=!一次;常数事件=这个_ events[type];//只有一个,让这个_events[type]`成为函数if(isNullOrUndefined(event)){ this ._ events[type]=fn;} else if(事件类型==' function '){//已经有一个函数,`这个._事件[类型]`必须是此之前的函数_events[type]=[event,fn];} else if (isArray(event)) { //已经有多个函数,只需推送这个即可。_事件[类型]。push(fn);}退回这个;};方法有四个参数,类型是监听事件的名称,fn是监听事件对应的方法,上下文俗称爸爸,改变这指向的,也就是执行的主体一次。是一个布尔型,用来标志是否只执行一次。首先判断【数学】函数的类型,如果不是方法,抛出一个类型错误fn.context=contextfn.once=!一次把执行主体和是否执行一次作为方法的属性常量事件=这个。_事件[类型]把该对应类型的所有已经监听的方法存到变量事件。

//只有一个,让这个_events[type]`成为函数if(isNullOrUndefined(event)){ this ._ events[type]=fn;} else if(事件类型==' function '){//已经有一个函数,`这个._事件[类型]`必须是此之前的函数_events[type]=[event,fn];} else if (isArray(event)) { //已经有多个函数,只需推送这个即可。_事件[类型]。push(fn);}退回这个;如果类型本身没有正在监听任何方法,这个事件[类型]=fn直接把监听的方法【数学】函数赋给类型属性;如果正在监听一个方法,则把要添加的【数学】函数和之前的方法变成一个含有2个元素的数组[事件,fn],然后再赋给类型属性,如果正在监听超过2个方法,直接推即可。最后返回这个,也就是事件发射器实例本身。

简单来讲不管是监听多少方法,都放到数组里是没必要像上面细分。但性能较差,只有一个方法时key: fn的效率比key: [fn]要高。

再回头看看三个方法:

addListener(类型,fn,上下文){ return _ addListener。调用(this、type、fn、context);{ on(type,fn,context){ return this。addlistener(类型、fn、上下文);}一次(类型,fn,上下文){ return _ addlistener。调用(this,type,fn,context,true);}添加侦听器需要用呼叫来改变这指向,指到了类的实例一次。则多传了一个标志位真实的来标志它只需要执行一次。这里你会看到我在添加侦听器并没有传错误的作为标志位,主要是因为我懒,但并不会影响到程序的逻辑。因为前面的fn.once=!一次已经能很好的处理不传值的情况。没传值!一次为假的。

接下来讲发射

发射(类型,rest){ if(isNullOrUndefined(type)){ 0抛出新的错误('发出必须接收至少一个参数');}常量事件=这个_ events[type];if (isNullOrUndefined(事件))返回falseif(事件类型==' function '){ events。调用(事件。上下文| | null,rest);if(事件。曾经){这个。removelistener(类型,事件);} } else if(IsArray(events)){ events。map(e={ e . call(e . context | | null,rest);如果(例如一次){这个。removelistener(类型,e);} });}返回真}事件触发需要指定具体的类型否则直接抛出错误。这个很容易理解,你都没有指定名称,我怎么知道该去执行谁的事件如果(isNullOrUndefined(事件))返回假的,如果类型对应的方法是不明确的或者null,直接返回假的。因为压根没有对应类型的方法可以执行。而发射需要知道是否被成功触发。

接着判断evnts是不是一个方法,如果是,事件。调用(事件。上下文| | null,rest)执行该方法,如果指定了执行主体,用呼叫改变这的指向指向事件。背景主体,否则指向null,全局环境。对于浏览器环境来说就是窗户。差点忘了休息,休息是方法执行时的其他参数变量,可以不传,也可以为一个或多个。执行结束后判断事件。一次,如果为没错,就用removeListener移除该监听事件。

如果evnts是数组,逻辑一样,只是需要遍历数组去执行所有的监听方法。

成功执行结束后返回真的。

removeListener(类型,fn) { if (isNullOrUndefined)(这事件)返回这个;//如果类型未定义或者为null,什么都不做,只返回这个if (isNullOrUndefined(类型))返回这个;if(fn的类型!==' function '){ 0引发新错误(' fn必须是函数');}常量事件=这个_ events[type];if(事件类型==' function '){事件===fn删除此_ events[type];} else { const find index=事件。find index(e=e===fn);if (findIndex===-1)返回这个;//匹配第一个,移位比拼接快if(find index===0){ events。shift();} else { events.splice(findIndex,1);} //只留下一个侦听器,如果(events.length===1) {这个,将排列更改为功能._ events[type]=events[0];} }返回此;}removeListener接收一个事件名称类型和一个将要被移除的方法fn。if (isNullOrUndefined(这个事件)返回这个这里表示如果事件发射器实例本身的_事件为空或者不明确的的话,没有任何事件监听,直接返回这个。

if (isNullOrUndefined(类型))返回这个如果没有提供事件名称,也直接返回这个。

if(fn的类型!==' function '){ 0引发新错误(' fn必须是函数');}fn如果不是一个方法,直接抛出错误,很好理解。

接着判断类型对应的事件是不是一个方法,是,并且事件===fn说明类型对应的方法有且仅有一个,等于我们指定要删除的方法。这个时候删除这个。_事件[类型]直接删除掉这个。_事件对象里类型即可。

所有的类型对应的方法都被移除后。想一想这个_events[type]=未定义和删除这个。_事件[类型]会有什么不同?

差别很大。这个。_events[type]=undefined仅在此中分配type属性。_events对象未定义。type属性仍然占用内存空间,但它是无用的。如果有多个类型,可能会导致内存泄漏。删除这个。_events[type]直接删除,不占用内存空间。前者也是Node.js事件模块和eventemitter3的早期实现。

如果events是一个数组,我使用else而不是isArray。原因是这个的输入。_events[type]仅限于on或一次,他们已经对此进行了限制。_events[type]似乎是一个由方法或一个方法组成的数组,最多是我们不小心或人为地赋了undefined或null,但我们之前也判断过这种情况。

因为isArray这种工具方法效率不高,为了追求一些效率,isArray不能在不影响操作逻辑的情况下使用。而事件的类型==='function '比isArray更能有效判断它是不是数组。Typeof也比object更有效率。原型。扔东西。调用(事件)==='[对象函数]。但是,不能通过typeof判断数组,因为它返回的是object,这是众所周知的。即便如此,在我采访的众多人中,还是有很多人不知道。

Const findIndex=事件。findindex (e=e===fn)这里,使用ES6数组方法findindex来直接查找事件中fn的索引。如果findIndex===-1表示我们还没有找到要删除的fn,那么就返回到这个。如果findIndex===0,则是数组的第一个元素,用shift消除,否则用拼接消除。因为移位比拼接更有效。

findIndex的效率没有for循环高,所以我在做基准测试之前就知道eventemitter8的效率会比eventemitter3低很多。当你不追求执行效率时,这当然是最酷的写作方式。所谓懒惰就是正义。

最后,我们要判断去除fn后的剩余事件数。如果只有一个,基于前面的优化,这个。_events[type]=events[0]将包含一个元素的数组转换为一个方法,并降低维度。

最后,return这个返回本身,链式调用仍然可以使用。

removeAllListeners(类型){ if (isNullOrUndefined(这。_events))返回这个;//如果不提供类型,则移除所有if (isNullOrUndefined(type))这个。_events=Object.create(空);常量事件=这个。_ events[type];if(!isNullOrUndefined(events)) { //检查类型是否是最后一个如果(Object.keys(this。_事件)。length===1) { this。_events=Object.create(空);} else {删除此内容。_ events[type];} }返回此;};RemoveAllListeners意味着删除与某个类型对应的所有方法。参数类型是可选的。如果未指定类型,默认情况下将删除所有监控事件。_ events=对象。create (null)可以直接操作,就像初始化EventEmitter类一样。

如果事件既不是null也不是未定义的,这意味着有一个可以删除的类型,则首先使用对象。钥匙(这个。_事件)。length===1判断是否是最后一种类型。如果是,直接初始化这个。_ events=对象。创建(空),否则删除它。_events[type]一步直接删除type属性。

最后回到这个。

到目前为止,所有的核心功能都已经完成。

侦听器(类型){ if (isNullOrUndefined(这。_events))返回[];常量事件=这个。_ events[type];//使用' map ',因为我们需要返回一个新的数组return isNullOrUndefined(events)?[] :(事件类型=='function '?[事件]: events . map(o=o));}listenerCount(类型){ if (isNullOrUndefined(此。_events))返回0;常量事件=这个。_ events[type];返回isNullOrUndefined(事件)?0 :(事件类型==='function '?1 :事件. length);} event name(){ if(isNullOrUndefined)(这。_events))返回[];返回Object.keys(这个。_事件);}侦听器返回与类型对应的所有方法。结果是一个数组,如果不是,返回一个空数组;如果只有一个,则将它的方法放在数组中并返回;如果是数组,map返回。使用map返回这个的原因。_events[type]是指map返回一个新数组,这是一个深度副本,修改数组中的值不会影响原始数组。这个。_events[type]返回原始数组的引用,这是一个浅拷贝。稍有不慎改变数值就会影响原数组。造成这种差异的根本原因是数组是引用类型,浅拷贝只是指针拷贝。你可以单独写一篇文章,不要展开。

ListenerCount返回类型对应的方法个数,代码一看就能理解。

EventNames返回由所有类型组成的数组,但不返回空数组;否则,它将直接与Object.keys一起返回。_事件)。

最后,导出默认事件发射器导出事件发射器。

结论

读了两个图书馆才知道怎么写。事实上,最好的学习方法是知道EventEmitter是干什么的,自己写。写完之后会和那些库对比,找出差距,再改正。

但不代表先读后写就没有收获,至少没有比读、不写、不读更大的收获。

水平有限,代码或文章不清晰的错误和遗漏不可避免。欢迎批评指正。

推荐阅读:

now.js:https://github.com/hongmaoxiao/now

更多资讯
游戏推荐
更多+