GitHub 地址:JavaScript EventEmitter
水平有限,欢迎批评指正
2 个多月前把 Github 上的 eventemitter3 和 Node.js
下的事件模块 events 的源码抄了一遍,才终于对 JavaScript
事件有所了解。
上个周末花点时间根据之前看源码的理解自己用 ES6 实现了一个 eventemitter8,然后也发布到 npm 上了,让我比较意外的是才发布两天在没有 readme
介绍,没有任何宣传的情况下居然有 45 个下载,我很好奇都是谁下载的,会不会用。我花了不少时间半抄半原创的一个 JavaScript
时间处理库 now.js (npm
传送门:now.js) ,在我大力宣传的情况下,4 个月的下载量才177。真是有心栽花花不开,无心插柳柳成荫
!
eventemitter8
大部分是我根据看源码理解后写出来的,有一些方法如listeners
,listenerCount
和 eventNames
一下子想不起来到底做什么,回头重查。测试用例不少是参考了 eventemitter3
,在此对 eventemitter3
的开发者们和 Node.js
事件模块的开发者们表示感谢!
下面来讲讲我对 JavaScript
事件的理解:
从上图可以看出,JavaScript
事件最核心的包括事件监听 (addListener)
、事件触发 (emit)
、事件删除 (removeListener)
。
首先,监听肯定要有监听的目标,或者说是对象,那为了达到区分目标的目的,名字是不可少的,我们定义为 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
是干什么用的以后自己动手写,写完以后再和那些库进行对比,找出差距,修正再修正。
但也不是说先看再写没有收获,至少比只看不写和看都没看的有收获不是。。。
水平有限,代码错漏或者文章讲不清楚之处在所难免,欢迎大家批评指正。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.