GitHub地址:JavaScript EventEmitterjavascript
博客地址:JavaScript EventEmitterhtml
水平有限,欢迎批评指正java
2个多月前把 Github 上的 eventemitter3 和 Node.js
下的事件模块 events 的源码抄了一遍,才终于对 JavaScript
事件有所了解。node
上个周末花点时间根据以前看源码的理解本身用 ES6 实现了一个 eventemitter8,而后也发布到 npm 上了,让我比较意外的是才发布两天在没有 readme
介绍,没有任何宣传的状况下竟然有45个下载,我很好奇都是谁下载的,会不会用。我花了很多时间半抄半原创的一个 JavaScript
时间处理库 now.js (npm
传送门:now.js) ,在我大力宣传的状况下,4个月的下载量才177。真是有心栽花花不开,无意插柳柳成荫
!git
eventemitter8
大部分是我根据看源码理解后写出来的,有一些方法如listeners
,listenerCount
和 eventNames
一会儿想不起来到底作什么,回头重查。测试用例很多是参考了 eventemitter3
,在此对 eventemitter3
的开发者们和 Node.js
事件模块的开发者们表示感谢!github
下面来说讲我对 JavaScript
事件的理解:面试
从上图能够看出,JavaScript
事件最核心的包括事件监听 (addListener)
、事件触发 (emit)
、事件删除 (removeListener)
。npm
首先,监听确定要有监听的目标,或者说是对象,那为了达到区分目标的目的,名字是不可少的,咱们定义为 type
。数组
其次,监听的目标必定要有某种动做,对应到 JavaScript
里实际上就是某种方法,这里定义为 fn
。浏览器
譬如能够监听一个 type
为 add
,方法为某一个变量 a
值加1
的方法 fn = () => a + 1
的事件。若是咱们还想监听一个使变量 b
加2
的方法,咱们第一反应多是建立一个 type
为 add2
,方法 为 fn1 = () => b + 2
的事件。你可能会想,这太浪费了,我能不能只监听一个名字,让它执行多于一个方法的事件。固然是能够的。
那么怎么作呢?
很简单,把监听的方法放在一个数组里,遍历数组顺序执行就能够了。以上例子变为 type
为 add
,方法为[fn, fn1]
。
若是要细分的话还能够分为能够无限次执行的事件 on
和 只容许执行一次的事件 once
(执行完后当即将事件删除)。待后详述。
单有事件监听是不够的,必需要有事件触发才能算完成整个过程。emit
就是去触发监听的特定 type
对应的单个事件或者一系列事件。拿前面的例子来讲单个事件就是去执行 fn
,一系列事件就是去遍历执行 fn
和 fn1
。
严格意义上来说,事件监听和事件触发已经能完成整个过程。事件删除无关紧要。但不少时候,咱们仍是须要事件删除的。好比前面讲的只容许执行一次事件 once
,若是不提供删除方法,很难保证你何时会再次执行它。一般状况下,只要是再也不须要的事件,咱们都应该去删除它。
核心部分讲完,下面简单的对 eventemitter8
的源码进行解析。
所有源码:
const toString = Object.prototype.toString;
const isType = obj => toString.call(obj).slice(8, -1).toLowerCase();
const isArray = obj => Array.isArray(obj) || isType(obj) === 'array';
const isNullOrUndefined = obj => obj === null || obj === undefined;
const _addListener = function(type, fn, context, once) {
if (typeof fn !== 'function') {
throw new TypeError('fn must be a function');
}
fn.context = context;
fn.once = !!once;
const event = this._events[type];
// only one, let `this._events[type]` to be a function
if (isNullOrUndefined(event)) {
this._events[type] = fn;
} else if (typeof event === 'function') {
// already has one function, `this._events[type]` must be a function before
this._events[type] = [event, fn];
} else if (isArray(event)) {
// already has more than one function, just push
this._events[type].push(fn);
}
return this;
};
class EventEmitter {
constructor() {
if (this._events === undefined) {
this._events = Object.create(null);
}
}
addListener(type, fn, context) {
return _addListener.call(this, type, fn, context);
}
on(type, fn, context) {
return this.addListener(type, fn, context);
}
once(type, fn, context) {
return _addListener.call(this, type, fn, context, true);
}
emit(type, ...rest) {
if (isNullOrUndefined(type)) {
throw new Error('emit must receive at lease one argument');
}
const events = this._events[type];
if (isNullOrUndefined(events)) return false;
if (typeof events === 'function') {
events.call(events.context || null, rest);
if (events.once) {
this.removeListener(type, events);
}
} else if (isArray(events)) {
events.map(e => {
e.call(e.context || null, rest);
if (e.once) {
this.removeListener(type, e);
}
});
}
return true;
}
removeListener(type, fn) {
if (isNullOrUndefined(this._events)) return this;
// if type is undefined or null, nothing to do, just return this
if (isNullOrUndefined(type)) return this;
if (typeof fn !== 'function') {
throw new Error('fn must be a function');
}
const events = this._events[type];
if (typeof events === 'function') {
events === fn && delete this._events[type];
} else {
const findIndex = events.findIndex(e => e === fn);
if (findIndex === -1) return this;
// match the first one, shift faster than splice
if (findIndex === 0) {
events.shift();
} else {
events.splice(findIndex, 1);
}
// just left one listener, change Array to Function
if (events.length === 1) {
this._events[type] = events[0];
}
}
return this;
}
removeAllListeners(type) {
if (isNullOrUndefined(this._events)) return this;
// if not provide type, remove all
if (isNullOrUndefined(type)) this._events = Object.create(null);
const events = this._events[type];
if (!isNullOrUndefined(events)) {
// check if `type` is the last one
if (Object.keys(this._events).length === 1) {
this._events = Object.create(null);
} else {
delete this._events[type];
}
}
return this;
}
listeners(type) {
if (isNullOrUndefined(this._events)) return [];
const events = this._events[type];
// use `map` because we need to return a new array
return isNullOrUndefined(events) ? [] : (typeof events === 'function' ? [events] : events.map(o => o));
}
listenerCount(type) {
if (isNullOrUndefined(this._events)) return 0;
const events = this._events[type];
return isNullOrUndefined(events) ? 0 : (typeof events === 'function' ? 1 : events.length);
}
eventNames() {
if (isNullOrUndefined(this._events)) return [];
return Object.keys(this._events);
}
}
export default EventEmitter;
复制代码
代码不多,只有151行,由于写的简单版,且用的 ES6
,因此才这么少;Node.js
的事件和 eventemitter3
可比这多且复杂很多,有兴趣可自行深刻研究。
const toString = Object.prototype.toString;
const isType = obj => toString.call(obj).slice(8, -1).toLowerCase();
const isArray = obj => Array.isArray(obj) || isType(obj) === 'array';
const isNullOrUndefined = obj => obj === null || obj === undefined;
复制代码
这4行就是一些工具函数,判断所属类型、判断是不是 null
或者 undefined
。
constructor() {
if (isNullOrUndefined(this._events)) {
this._events = Object.create(null);
}
}
复制代码
建立了一个 EventEmitter
类,而后在构造函数里初始化一个类的 _events
属性,这个属性不须要要继承任何东西,因此用了 Object.create(null)
。固然这里 isNullOrUndefined(this._events)
还去判断了一下 this._events
是否为 undefined
或者 null
,若是是才须要建立。但这不是必要的,由于实例化一个 EventEmitter
都会调用构造函数,皆为初始状态,this._events
应该是不可能已经定义了的,可去掉。
addListener(type, fn, context) {
return _addListener.call(this, type, fn, context);
}
on(type, fn, context) {
return this.addListener(type, fn, context);
}
once(type, fn, context) {
return _addListener.call(this, type, fn, context, true);
}
复制代码
接下来是三个方法 addListener
、on
、once
,其中 on
是 addListener
的别名,可执行屡次。once
只能执行一次。
三个方法都用到了 _addListener
方法:
const _addListener = function(type, fn, context, once) {
if (typeof fn !== 'function') {
throw new TypeError('fn must be a function');
}
fn.context = context;
fn.once = !!once;
const event = this._events[type];
// only one, let `this._events[type]` to be a function
if (isNullOrUndefined(event)) {
this._events[type] = fn;
} else if (typeof event === 'function') {
// already has one function, `this._events[type]` must be a function before
this._events[type] = [event, fn];
} else if (isArray(event)) {
// already has more than one function, just push
this._events[type].push(fn);
}
return this;
};
复制代码
方法有四个参数,type
是监听事件的名称,fn
是监听事件对应的方法,context
俗称爸爸
,改变 this
指向的,也就是执行的主体。once
是一个布尔型,用来标志是否只执行一次。 首先判断 fn
的类型,若是不是方法,抛出一个类型错误。fn.context = context;fn.once = !!once
把执行主体和是否执行一次做为方法的属性。const event = this._events[type]
把该对应 type
的全部已经监听的方法存到变量 event
。
// only one, let `this._events[type]` to be a function
if (isNullOrUndefined(event)) {
this._events[type] = fn;
} else if (typeof event === 'function') {
// already has one function, `this._events[type]` must be a function before
this._events[type] = [event, fn];
} else if (isArray(event)) {
// already has more than one function, just push
this._events[type].push(fn);
}
return this;
复制代码
若是 type
自己没有正在监放任何方法,this._events[type] = fn
直接把监听的方法 fn
赋给 type
属性 ;若是正在监听一个方法,则把要添加的 fn
和以前的方法变成一个含有2个元素的数组 [event, fn]
,而后再赋给 type
属性,若是正在监听超过2个方法,直接 push
便可。最后返回 this
,也就是 EventEmitter
实例自己。
简单来说不论是监听多少方法,都放到数组里是不必像上面细分。但性能较差,只有一个方法时 key: fn
的效率比 key: [fn]
要高。
再回头看看三个方法:
addListener(type, fn, context) {
return _addListener.call(this, type, fn, context);
}
on(type, fn, context) {
return this.addListener(type, fn, context);
}
once(type, fn, context) {
return _addListener.call(this, type, fn, context, true);
}
复制代码
addListener
须要用 call
来改变 this
指向,指到了类的实例。once
则多传了一个标志位 true
来标志它只须要执行一次。这里你会看到我在 addListener
并无传 false
做为标志位,主要是由于我懒,但并不会影响到程序的逻辑。由于前面的 fn.once = !!once
已经能很好的处理不传值的状况。没传值 !!once
为 false
。
接下来说 emit
emit(type, ...rest) {
if (isNullOrUndefined(type)) {
throw new Error('emit must receive at lease one argument');
}
const events = this._events[type];
if (isNullOrUndefined(events)) return false;
if (typeof events === 'function') {
events.call(events.context || null, rest);
if (events.once) {
this.removeListener(type, events);
}
} else if (isArray(events)) {
events.map(e => {
e.call(e.context || null, rest);
if (e.once) {
this.removeListener(type, e);
}
});
}
return true;
}
复制代码
事件触发须要指定具体的 type
不然直接抛出错误。这个很容易理解,你都没有指定名称,我怎么知道该去执行谁的事件。if (isNullOrUndefined(events)) return false
,若是 type
对应的方法是 undefined
或者 null
,直接返回 false
。由于压根没有对应 type
的方法能够执行。而 emit
须要知道是否被成功触发。
接着判断 evnts
是否是一个方法,若是是, events.call(events.context || null, rest)
执行该方法,若是指定了执行主体,用 call
改变 this
的指向指向 events.context
主体,不然指向 null
,全局环境。对于浏览器环境来讲就是 window
。差点忘了 rest
,rest
是方法执行时的其余参数变量,能够不传,也能够为一个或多个。执行结束后判断 events.once
,若是为 true
,就用 removeListener
移除该监听事件。
若是 evnts
是数组,逻辑同样,只是须要遍历数组去执行全部的监听方法。
成功执行结束后返回 true
。
removeListener(type, fn) {
if (isNullOrUndefined(this._events)) return this;
// if type is undefined or null, nothing to do, just return this
if (isNullOrUndefined(type)) return this;
if (typeof fn !== 'function') {
throw new Error('fn must be a function');
}
const events = this._events[type];
if (typeof events === 'function') {
events === fn && delete this._events[type];
} else {
const findIndex = events.findIndex(e => e === fn);
if (findIndex === -1) return this;
// match the first one, shift faster than splice
if (findIndex === 0) {
events.shift();
} else {
events.splice(findIndex, 1);
}
// just left one listener, change Array to Function
if (events.length === 1) {
this._events[type] = events[0];
}
}
return this;
}
复制代码
removeListener
接收一个事件名称 type
和一个将要被移除的方法 fn
。if (isNullOrUndefined(this._events)) return this
这里表示若是 EventEmitter
实例自己的 _events
为 null
或者 undefined
的话,没有任何事件监听,直接返回 this
。
if (isNullOrUndefined(type)) return this
若是没有提供事件名称,也直接返回 this
。
if (typeof fn !== 'function') {
throw new Error('fn must be a function');
}
复制代码
fn
若是不是一个方法,直接抛出错误,很好理解。
接着判断 type
对应的 events
是否是一个方法,是,而且 events === fn
说明 type
对应的方法有且仅有一个,等于咱们指定要删除的方法。这个时候 delete this._events[type]
直接删除掉 this._events
对象里 type
便可。
全部的 type
对应的方法都被移除后。想想 this._events[type] = undefined
和 delete this._events[type]
会有什么不一样?
差别是很大的,this._events[type] = undefined
仅仅是将 this._events
对象里的 type
属性赋值为 undefined
,type
这一属性依然占用内存空间,但其实已经没什么用了。若是这样的 type
一多,有可能形成内存泄漏。delete this._events[type]
则直接删除,不占内存空间。前者也是 Node.js
事件模块和 eventemitter3
早期实现的作法。
若是 events
是数组,这里我没有用 isArray
进行判断,而是直接用一个 else
,缘由是 this._events[type]
的输入限制在 on
或者 once
中,而它们已经限制了 this._events[type]
只能是方法组成的数组或者是一个方法,最多加上不当心或者人为赋成 undefined
或 null
的状况,但这个状况咱们也在前面判断过了。
由于 isArray
这个工具方法其实运行效率是不高的,为了追求一些效率,在不影响运行逻辑状况下能够不用 isArray
。并且 typeof events === 'function'
用 typeof
判断方法也比 isArray
的效率要高,这也是为何不先判断是不是数组的缘由。用 typeof
去判断一个方法也比 Object.prototype.toSting.call(events) === '[object Function]
效率要高。但数组不能用 typeof
进行判断,由于返回的是 object
, 这众所周知。虽然如此,在我面试过的不少人中,仍然有不少人不知道。。。
const findIndex = events.findIndex(e => e === fn)
此处用 ES6
的数组方法 findIndex
直接去查找 fn
在 events
中的索引。若是 findIndex === -1
说明咱们没有找到要删除的 fn
,直接返回 this
就好。若是 findIndex === 0
,是数组第一个元素,shift
剔除,不然用 splice
剔除。由于 shift
比 splice
效率高。
findIndex
的效率其实没有 for
循环去查找的高,因此 eventemitter8
的效率在我没有作 benchmark
以前我就知道确定会比 eventemitter3
效率要低很多。不那么追求执行效率时固然是用最懒的方式来写最爽。所谓的懒即正义
。。。
最后还得判断移除 fn
后 events
剩余的数量,若是只有一个,基于以前要作的优化,this._events[type] = events[0]
把含有一个元素的数组变成一个方法,降维打击一下。。。
最后的最后 return this
返回自身,链式调用还能用得上。
removeAllListeners(type) {
if (isNullOrUndefined(this._events)) return this;
// if not provide type, remove all
if (isNullOrUndefined(type)) this._events = Object.create(null);
const events = this._events[type];
if (!isNullOrUndefined(events)) {
// check if type is the last one
if (Object.keys(this._events).length === 1) {
this._events = Object.create(null);
} else {
delete this._events[type];
}
}
return this;
}
复制代码
removeAllListeners
指的是要删除一个 type
对应的全部方法。参数 type
是可选的,若是未指定 type
,默认把全部的监听事件删除,直接 this._events = Object.create(null)
操做便可,跟初始化 EventEmitter
类同样。
若是 events
既不是 null
且不是 undefined
说明有可删除的 type
,先用 Object.keys(this._events).length === 1
判断是否是最后一个 type
了,若是是,直接初始化 this._events = Object.create(null)
,不然 delete this._events[type]
直接删除 type
属性,一步到位。
最后返回 this
。
到目前为止,全部的核心功能已经讲完。
listeners(type) {
if (isNullOrUndefined(this._events)) return [];
const events = this._events[type];
// use `map` because we need to return a new array
return isNullOrUndefined(events) ? [] : (typeof events === 'function' ? [events] : events.map(o => o));
}
listenerCount(type) {
if (isNullOrUndefined(this._events)) return 0;
const events = this._events[type];
return isNullOrUndefined(events) ? 0 : (typeof events === 'function' ? 1 : events.length);
}
eventNames() {
if (isNullOrUndefined(this._events)) return [];
return Object.keys(this._events);
}
复制代码
listeners
返回的是 type
对应的全部方法。结果都是一个数组,若是没有,返回空数组;若是只有一个,把它的方法放到一个数组中返回;若是原本就是一个数组,map
返回。之因此用 map
返回而不是直接 return this._events[type]
是由于 map
返回一个新的数组,是深度复制,修改数组中的值不会影响到原数组。this._events[type]
则返回原数组的一个引用,是浅度复制,稍不当心改变值会影响到原数组。形成这个差别的底层缘由是数组是一个引用类型,浅度复制只是指针拷贝。这能够单独写一篇文章,不展开了。
listenerCount
返回的是 type
对应的方法的个数,代码一眼就明白,很少说。
eventNames
这个返回的是全部 type
组成的数组,没有返回空数组,不然用 Object.keys(this._events)
直接返回。
最后的最后,export default EventEmitter
把 EventEmitter
导出。
我是先看了两个库才知道怎么写的,其实最好的学习方法是知道 EventEmitter
是干什么用的之后本身动手写,写完之后再和那些库进行对比,找出差距,修正再修正。
但也不是说先看再写没有收获,至少比只看不写和看都没看的有收获不是。。。
水平有限,代码错漏或者文章讲不清楚之处在所不免,欢迎你们批评指正。