半个月前看到一篇文章将eventEmitter,看完之后心血来潮自己写了一个。晚上睡觉前忽然想到还可以尝试实现vue中emitter。于是,故事就这么开始了。
1、实现一个eventEmiiter
1.1、整体架构
我们先看一张图,看下EventEmitter
需要实现哪些功能
可以看到一个EventEmitter
类的内容其实并不杂乱。根据这张图来看,我们大致可以分为以下几个模块。
EventEmitter初始属性
- _events //存储所有的监听器
- _maxListeners setMaxListeners getMaxListeners
addEventListener模块
- addListener
- prependListener
- once
- prependOnceListener
- on
emit模块
- emitNone
- emitOne emitTwo emitThree
- emitMany
- error 事件
removeEventListener模块
- removeListener
- removeAllListeners
listeners,eventNames
- listeners //获取一个监听器下的所有事件
- eventNames //获取有哪些监听器
工具函数和兼容性函数
- spliceOne
- arrayClone
- objectCreatePolyfill
- objectKeysPolyfill
- functionBindPolyfill
基本上按照上面这个顺序,就可以写出来一个基本的的eventEmitter
的类了。
推荐大家可以先自己尝试写一写,这样子等下看成熟库的源码可以得到的收获更多。
然后去网上找了一个成熟库的源码进行对比,果然发现了一些问题需要改善😳。
点这里。写完之后,可以看下看EventEmitter类源码怎么实现的。
- 自己为了节省代码行数,单个事件和多个事件都用了Array去存储。其实作为库,节约的十几行代码和性能比起来,还是后者更重要
- 没有考虑emit几个参数的情况,不同情况的处理有助于提高性能
- 没有考虑限制一个类可以绑定的最大事件数。因为如果数目一多话,容易造成内存泄露.
- 函数缺少对参数的判断。缺少防御性代码
1.2、简单分析一下部分代码
具体代码就不分析了😂😂,稍微对主线讲解一下吧。因为源码并不复杂,沉下心花个半小时肯定能全部看懂。
作者一开始新建一个_events
对象,这个对象会在后期存取我们监听器。然后设定了一个监听器允许的最大事件,避免内存泄露的可能性。
然后添加事件,在真实场景中,我们会在html中获取需要添加哪些监听器type
,和对应的方法listener
当我们事件添加完毕之后,则是通过emit
进行调用
主线代码就这样,更多细节我是真的推荐大家全看源码的。因为这400多行的代码真的不复杂。反倒是源码中间还是有很多细节可以值得细细品味的。
1.2.1、为什么使用Object.create(null)
我们可以看到网上许多库(比如vue)都是使用Object.create(null)
来创建对象,而不是使用{}
来新建对象。这是为什么呢🤔?
Object.create()这个API我就不介绍了,不清楚的推荐上MDN先了解一下
我们可以先在chrome的控制台上打印Object.create({})
创建的对象是什么样子的:
可以看到新创建出来的对象继承了Object
原型链上所有的方法。
我们可以再看一下使用Object.create(null)
创建出来的对象:
没有任何属性,显示No properties。
区别很明显,我们获得了一个非常纯净的对象。那么问题来了,这样对象有什么好处呢🤔?
首先我们需要知道无论是var a = {}
还是Object.create({})
,他们返回的对象都是继承自Object
的原型,而原型是可以修改的。但是假如有别的库或者开发者在页面上修改了Object
的原型,那么你也会继承下来修改后的原型方法,这个可能就不是我们想要的了。
随手在一个csdn的网页控制台写个例子,没想到就出现这个问题
而如果我们自己在每个库开头新建一个干净的对象,我们可以自己改写这个对象的原型方法进行复用和继承,既不会影响他人,也不会被他人影响。
1.2.2、比原生splice效率还高的函数
在源码中看到了这么一段代码,作者亲自打了注释说1.5倍速度快于原生。
splice
的效率慢我是知道,但是作者说1.5倍快我就要亲自试验下了。
看他方法,缺陷应该是数组长度越长,所需时间越长;下标越靠近开始,所需时间越长。于是我用了不同长度的数组,不同下标去进行反复测试100次。
|
|
因为源码是针对node.js的,不知道是不是浏览器内部对splice
做过优化。作者的方法在特定情况下的确是做到了更快,还是很厉害的。👍👍👍🤕
1.2.3、多个emit方法
源码作者专门为emit不同数量参数写了不同的方法,有emitNone
,emitOne
,emitTwp
,emitThree
,emitMany
。
如果按照我来写,最多也就分成emitNone
和emitMany
两个方法。但是作者应该是为了更高的效率,尽可能减少for循环这种代码。这也是我这种不怎么写库的人迟钝的地方。节约的十几行代码在压缩之后,重要性是低于性能上的损耗的。
2、简单实现vue中的EventEmitter
在写完EventEmitter
之后,仍然感觉特别单调。然后睡觉的时候忽然在想,是不是可以正好将自己写好这个类套进到vue里面呢?有了实际场景,就知道自己写的东西到底能干什么,有什么问题。不然空有理论也是没有任何进步的。
之前网上也有很多文章解析了vue如何实现双向绑定。事实上在编译html的过程中实现了的不仅仅是数据双向绑定,添加事件监听器也是这一过程做的。只是网上关于事件监听的文章却几乎没有。
2.1、自己尝试实现一个vue中的EventEmitter
按照我一开始的想法,应该是先编译HTML获取所有的属性,判断出哪些属性是绑定事件,哪些是数据绑定。
于是自己先写出了第一段代码,希望依靠原生node的方法attributes
去获取DOM元素上所有的属性
但是等到获取之后,才发现获取到的每个属性attr[i]
竟然是一个神奇的对象类型[object Attr]
,表现形式是@click=test
。虽然表现很像是字符串,但是个NamedNodeMap
。靠根本不知道怎么用嘛😂😂😂
去网上找了资料之后,才知道他是怎么获取key和value的。
只是文章里面说DOM4规定中已经不推荐使用这个属性了😢ㄟ( ▔, ▔ )ㄏ。想了想放弃了,还是乖乖去看了一下vue的源码是怎么实现的吧。
2.2、vue源码实现一个EventEmitter
因为想着vue肯定也是先编译HTML,所以直接找到了源码中的html-parse
模块。
vue先定义了一个parseHTML
的方法,传进来需要编译的html模板,也就是我们的template
。然后通过一个属性正则表达式一步步去match出模板字符串内的所有属性,最后返回了一个包含所有属性的数组attrs
。
然后vue会对得到的数组attrs
进行遍历判断,这个属性是v-for
?还是change
?还是src
等等。当获取到的属性为@click
或者v-on:click
这种事件之后,然后通过方法addHandler
去添加事件监听器。我们也就可以在开发中使用emit
了。
当然vue中间还会有很多操作。比如会接着将这个属性数组以及tag传入到一个
createASTElement
函数里面进行生成一棵AST树渲染成真实的dom等等。只不过这并不是我们本篇文章需要讨论的内容了
我们接下去就按照vue的流程来实现绑定事件。首先我们定义好我们的html内容。
在我们就要开始进行编译之前,我们准备好所有需要用到的正则,新建好一个eventEmitter
类
|
|
然后开始写我们的编译函数。前面已经说了,我们传进模板,然后依据正则一步步match出所有的属性.
解释一下编译过程吧。先根据开头标签的正则,找到需要编译的html。然后截取出除开始标签<div
的剩余字符串。
接下来继续对字符串判断。依靠属性正则表达式,判断这段html标签内有没有属性,如果有的话,从字符串中截取(类似于splice效果)出来。
继续不断循环字符串,直到遇到闭合标签/div>
为止。然后结束编译,返回数组。
编译完成后我们已经获取到了模板里面所有的属性,但是现在存储起来的属性表现形式是一个match出来的数组,并不是一个方便开发者使用的map形式。所以接下来我们要处理一下我们获得的数组。
通过这一步,我们已经获取到了一个attrList
,并且存储起来了一个个表现为map形式的属性。然后我们要遍历这些属性,判断哪些是需要绑定的方法,哪些使我们不需要的属性。如果是需要绑定的方法,我们通过addHandler
函数来添加事件监听器。
走到这里整个流程就已经结束了。接下去每次进入页面去进行初始化编译就好了。
如果想尝试触发我们之前绑定的事件,在vue中是子组件向父组件触发。这里就不搞父子组件这么麻烦了。我们可以直接在JS里面调用emit
来进行验证
game over 😊
文章结束了,日常总结一下吧。实现整个eventEmitter的代码其实并不复杂,尤其在源码非常简洁的情况下,基本上认真看个十几分钟就能明白整个轮廓。然后我没有仔细看vue中的实现是怎么样的,不过我猜测应该相似度很高。
后面看vue提取属性还是花了更多的时间,原来还以为可以自己通过attribute属性来实现的。没想到最后还是参考了vue,再看的途中,也明白了vue编译html的整个过程,以及每个过程实现了哪些内容。
其实看源码可以学到的东西都很多,最直接的就是知道怎么实现一个功能。此外呢?其实此外是是更多的。比如编码习惯,比如防御性代码怎么写,比如结尾处理代码怎么写,比这不就看到有方法比原生API的效率还快。这些都是看源码的乐趣所在。
看完之后,以后妈妈再也不用担心面试考eventEmitter了