我们在之前的文章中讲解中发布订阅者模式,不太了解这种模式的,可以先点击查看之前的文章浅谈javascript设计模式之发布订阅者模式。从发布订阅者模式的思想出发,来实现一个EventEmitter模块。
在实现一些基础类库时,我们是经常需要用到EventEmitter
模块的,比如在微信中下载某个APP时,前端需要了解当前下载处在哪个状态:刚开始下载、在在下载中、下载的进度、下载已完成、下载失败等等,都是需要我们向发布事件的。那么对应的就是实现emit
和on
。
1. 简单的实现
一个简单的EventEmitter模块,是只至少要实现emit和on两个方法:
COPYJAVASCRIPT
class EventEmitter { handles = {}; on(evName, listener) { if (!this.handles[evName]) { this.handles[evName] = []; } this.handles[evName].push(listener); } emit(evName, ...args) { const handler = this.handles[evName]; if (handler) { for(let i=0, len=handler.length; i<len; i++) { handler[i](...args); } } } }
这样就完成了一个简单的EventEmitter基类,这个基类可以自己单独使用:
COPYJAVASCRIPT
const downapp = new EventEmitter(); downapp.on('steps', step => console.log(step)); // 监听下载的进度 downapp.emit('steps', 0); // 输出: 0 downapp.emit('steps', 30); // 输出: 30 downapp.emit('steps', 100); // 输出; 100
也可以被其他类继承后使用:
COPYJAVASCRIPT
class People extends EventEmitter { constructor() { super(); } start() { let num = 10; let timer = setInterval(() => { this.emit('countdown', num--); if (num<=0) { clearInterval(timer); } }, 1000) } } const Tom = new People(); Tom.on('countdown', num => console.log('Tom', num)); Tom.start(); const Jerry = new People(); Jerry.on('countdown', num => console.log('Jerry', num)); Jerry.start();
2. 对EventEmitter扩展方法
基于emit
和on
我们可以扩展出多个方法,方便使用:
方法名 | 说明 |
---|---|
on(eventName, listener) | 监听所有触发的事件 |
emit(eventName, ...args) | 主动触发事件 |
once(eventName, listener) | 与on 事件类似,但只监听一次事件 |
off(eventName, listener) | 移除某个事件 |
offAll([eventName]) | 移除所有的事件,若传入具体的eventName,则移除该名称下所有的事件 |
newListener | 有添加新的监听器时触发 |
removeListener | 有监听器被移除时触发 |
2.1 off
在如何实现once的方法时,必须得先知道事件是如何移除的:
COPYJAVASCRIPT
class EventEmitter { off(evName, listener) { if (this.handles[evName]) { // 从数组中移除lisnter this.handles[evName] = this.handles[evName].filter(fn => fn!==listener); } } } // 接上面的People类 // const Tom = new People(); const listener = (num) => { console.log('Tom', num) } const listener2 = (num) => { console.log('Tom', num*num); } Tom.on('countdown', listener); Tom.on('countdown', listener2); Tom.on('countdown', listener); Tom.off('countdown', listener); // 移除所有的listener Tom.start(); // 只有listener有输出
通过Array.prototype.filter方法,过滤掉所有要移除的监听器。注意,跟为DOM添加和移除事件一样,不能传入匿名函数,例如下面的例子是不生效的,因为两个匿名函数虽然从函数体上看起来一样,但两者是不相等的:
COPYJAVASCRIPT
Tom.on('countdown', (num) => {console.log(num)}); Tom.off('countdown', (num) => {console.log(num)}); // 移除 Tom.start(); // 依然有输出
2.2 once
once,只要监听到第1个事件后,则立刻移除监听器,后续不再监听此事件:
COPYJAVASCRIPT
Tom.once('countdown', (num) => {console.log(num)}); // 10 Tom.start();
countdown事件会从10每秒输出一个数字,至到0为止。但我们使用once后,只输出了一个10后,则不再有后续的输出,说明监听器已经被移除。
2.3 newListener和removeListener
这两者都是表示监听器有变化时,程序主动触发的:
COPYJAVASCRIPT
class EventEmitter { on(evName, listener) { if (!this.handles[evName]) { this.handles[evName] = []; } this.handles[evName].push(listener); // 有新增事件时,触发newListener this.emit('newListener', evName, listener); return this; } off(evName, listener) { if (this.handles[evName]) { // 从数组中移除lisnter this.handles[evName] = this.handles[evName].filter(fn => { if (fn!==listener) { // 监听器有移除时,触发removeListener this.emit('removeListener', evName, listener); return true; } return false; }) } } }
那么这两个如何使用的呢,其实once这一个监听事件,就能触发上面的这两个监听器。先添加一个countdown监听器,执行一次监听后,马上移除countdown中的监听器:
COPYJAVASCRIPT
const listener = (num) => { console.log('Tom', num) } Tom.on('newListener', (evName, listener) => console.log('newListener', evName, listener) ); Tom.on('removeListener', (evName, listener) => console.log('removeListener', evName, listener) ); Tom.once('countdown', listener); Tom.start();
3. 完整的EventEmitter模块
我们从上面的章节中,已经了解了EventEmitter的原理,并实现了几个常用的模块。下面我们把完成的代码展示出来:
COPYJAVASCRIPT
class EventEmitter { handles = {}; on(evName, listener) { if (!this.handles[evName]) { this.handles[evName] = []; } this.handles[evName].push(listener); this.emit('newListener', evName, listener); return this; } once(evName, listener) { let fired = false; let magic = (...args) => { this.off(evName, magic); if (!fired) { fired = true; listener.apply(this, args); } } this.on(evName, magic); return this; } off(evName, listener) { if (this.handles[evName]) { // 从数组中移除lisnter this.handles[evName] = this.handles[evName].filter(fn => { if (fn!==listener) { this.emit('removeListener', evName, listener) return true; } return false; }) } } emit(evName, ...args) { const handler = this.handles[evName]; if (handler) { for(let i=0, len=handler.length; i<len; i++) { handler[i].apply(this, args); } } } }
4. 总结
EventEmitter功能我们经常有使用到,尤其是在实现的一些基础类库中,用到的更多。Vue中的bus功能也是这种思想。