基于观察者模式实现一个EventEmitter类

蚊子前端博客
发布于 2019-05-09 01:34
在实现一些基础类库时,我们是经常需要用到`EventEmitter`模块的,比如在微信中下载某个APP时,前端需要了解当前下载处在哪个状态:刚开始下载、在在下载中、下载的进度、下载已完成、下载失败等等,都是需要我们向发布事件的

我们在之前的文章中讲解中发布订阅者模式,不太了解这种模式的,可以先点击查看之前的文章浅谈javascript设计模式之发布订阅者模式。从发布订阅者模式的思想出发,来实现一个EventEmitter模块。

在实现一些基础类库时,我们是经常需要用到EventEmitter模块的,比如在微信中下载某个APP时,前端需要了解当前下载处在哪个状态:刚开始下载、在在下载中、下载的进度、下载已完成、下载失败等等,都是需要我们向发布事件的。那么对应的就是实现emiton

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扩展方法

基于emiton我们可以扩展出多个方法,方便使用:

方法名说明
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功能也是这种思想。

标签:
阅读(1123)

公众号:

qrcode

微信公众号:前端小茶馆

公众号:

qrcode

微信公众号:前端小茶馆