先看如下代码:
00// test.js 01module.exports = test; 02 03function test(my_event_bus){ 04 console.log('监听事件 喵'); 05 my_event_bus.on('喵', function(e){ 06 console.log('喵1, 参数:', e); 07 }); 08 09 my_event_bus.on('喵', function(e){ 10 console.log('喵2, 参数:', e); 11 }); 12 13 my_event_bus.addListener('喵', function(e){ 14 console.log('喵3, 参数: ', e); 15 }); 16 17 setTimeout(() => { 18 my_event_bus.emit('喵', '啦!'); 19 20 console.log('on 和 addListener 是不是一样的 ? '); 21 console.log(my_event_bus.on === my_event_bus.addListener); 22 }, 1500); 23}
按照下面的方式调用
test.js
00// native-test 01const { EventEmitter } = require('events'); 02const test = require('./test'); 03 04let my_event = new EventEmitter(); 05 06test(my_event);
输出结果如下
00监听事件 喵 01喵1, 参数: 啦! 02喵2, 参数: 啦! 03喵3, 参数: 啦! 04on 和 addListener 是不是一样的 ? 05true
# 自己实现一个 EventEmitter ↵
EventEmitter 实现了 on 来监听一个事件,实现了 emit 来触发事件,执行监听器。
自己实现一个,应该也不难,如下:
00// 实现一个 EventEmitter 01class Emily { 02 constructor(){ 03 // 用来保存事件以及事件监听器的对象 04 this.bus = {} 05 } 06 07 on(evt_name, fn){ 08 // 某事件的监听队列 09 let listeners = this.bus[evt_name]; 10 11 // 若这个监听队列不存在,则创建一个空队列 12 if (!listeners) this.bus[evt_name] = []; 13 14 // 插入到队尾 15 this.bus[evt_name].push(fn); 16 17 // 链式调用 18 return this; 19 } 20 21 emit(evt_name, evt_param){ 22 let listeners = this.bus[evt_name]; 23 24 // 说明监听队列不存在。 不报错直接 return this; 25 if (!listeners) return this; 26 27 // 否则执行监听器 28 listeners.forEach(fn => { 29 fn(evt_param); 30 }); 31 32 return this; 33 } 34} 35 36// 别名 37Emily.prototype.addListener = Emily.prototype.on; 38 39// 暴露 40module.exports = Emily;
然后编写 emily-test.js
00const Emily = require('./Emily') 01const test = require('./test'); 02 03let my_event_bus = new Emily(); 04 05test(my_event_bus);
结果与一开始的一样,没什么问题。
# 意味深长 ↵
EventEmitter 其实是很简洁的东西,这种简洁体现在它对真实世界关于事件的抽象:监听注册、触发执行。
理解了事件,也就理解了 EventEmitter,但似乎单个 EventEmitter 类似乎没有什么用,其实最佳的用法是继承它,一个类只要基础了它,就拥有了基础的事件驱动能力 (监听、触发)。
很多地方都能见到 on 和 emit 的身影,前端有 Vue :
00let bus = new Vue(); 01 02bus.$on('wow', e => console.log('wow', e)); 03bus.$emit('wow', '喵'); 04// => 05// wow 喵
更不用说 node 里各种 IO 操作了,比如 http 里的 request 的 on 方法监听事件,socketio 里的各种监听、触发。
这种关于事件的抽象,我们可以选择自己实现一个类似 EventEmitter 的构造,也可以直接让我们的对象继承自 EventEmitter 使其拥有事件驱动能力。
下面的 Alice.js 是一个继承 EventEmitter 的例子:
00const events = require('events'); 01const { EventEmitter } = events; 02 03class Alice extends EventEmitter { 04 constructor(el){ 05 // 调用父级构造器 06 super(); 07 } 08 09 sayHello(){ 10 console.log('hello'); 11 } 12} 13 14module.exports = Alice;
JS 里很多的库,你顺着他们的原型链找上去,不出意外都可以看到 EventEmitter 的魅影,它非常重要,除了上面提到的 on 和 emit 方法,它还有很多其他的方法,但只要能理解事件,这些方法都非常容易接受,在此不多说。
# 在浏览器上应用 EventEmitter 的哲学 ↵
DOM 可以触发很多事件,比如各种鼠标的事件,键盘的事件,但依然不够用,比如没有
DOM 被双击
这个事件,但是我们可以自己实现,我们可以给单个 DOM 绑定点击事件来实现双击的侦测,然后做相应的处理,但是这样要在多个 DOM 节点应用就有点麻烦,因此还需要进一步的封装。以下,实现一个 DOM 的双击事件,使用方式如下:
00// 在下面实现 double: 让 dom 能触发双击事件的修饰函数 01let double = require('./double'); 02let header = document.querySelector('#header'); 03 04// 修饰 05double(header); 06 07console.log(header) 08 09header.addEventListener('double-click', function(e){ 10 console.log('被双击', e); 11});
以下是
double
的实现00/** 01 * @description 让 dom 能触发双击事件的修饰函数 02 * @param { Element } dom 03 */ 04function double(dom){ 05 let speed = 0; 06 07 dom.addEventListener('click', function(){ 08 speed = speed + 1; 09 10 setTimeout(() => { 11 speed = speed - 1 12 }, 300); 13 }); 14 15 dom.addEventListener('click', function(click_event){ 16 if (speed >= 2){ 17 // 触发双击 18 console.log('! catch 双击,现在应该构造双击事件,并派发到 dom 去'); 19 20 // click_event 其实也是 MouseEvent 的实例 21 let doubleClick = new MouseEvent('double-click'); 22 23 // 派发 24 dom.dispatchEvent(doubleClick); 25 } 26 }); 27}
addEventlistener 相当于 EventEmitter 的 on;dispatchEvent 相当于 emit,它们是 EventTarget 上的方法,实现了对事件驱动的抽象。
而 Evnet 对象,其实是 on 的时候的一种参数,它指明了事件发生的各种参数,比如该 event 实例应该派发到哪个事件名,或者鼠标单击的坐标等等。