EventEmitter简单实现

NodeEvents 模块只定义了一个类,就是 EventEmitter,这个类在很多Node 本身以及第三方模块中大量使用,通常是用作基类被继承。

本内容,我们来把EventEmitterononceoffemit方法进行简单的手写实现。

Events模块基本使用

events模块大家使用思路基本都一致,都是继承出来使用,但是实现方式多种多样,这里默认大家都知道events模块的使用方式,不再赘述,贴一种基本使用方法吧,让不清楚使用方式的朋友有一个大概了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const EventEmitter = require('events')

// 我们一般不会直接使用EventEmitter,而是生成一个类继承EventEmitter
function MyFun() { }

MyFun.prototype = Object.create(EventEmitter.prototype)

let myFun = new MyFun()

// 订阅事件
myFun.on('eventName', () => { })
myFun.on('eventName', () => { })

// 发布事件
myFun.emit('eventName')

Events手写实现

咱们会对基础功能简单的手写实现,就里就不用Function写法直接采用ES6Class语法来写了。

on与emit方法实现

咱们通过on方法订阅事件,通过emit方法来发布事件,按照这个思路,不难写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class EventEmitter {
constructor() {
// 用于收集订阅的事件和对应回调函数
this._events = {}
}
/**
* @description 订阅事件
* @param {string} eventName 事件名
* @param {function} callback 回调函数
*/
on(eventName, callback) {
if (this._events[eventName]) {
this._events[eventName].push(callback)
} else {
this._events[eventName] = [callback]
}
}
/**
* @description 发布事件
* @param {string} eventName 事件名
* @param {*} args 其他参数
*/
emit(eventName, ...args) {
// 找到对应事件的处理函数以此调用
this._events[eventName].forEach(fn => {
fn(...args)
})
}
}

看起来好像很顺利,但是不要忽略的一个问题,我们继承的时候只是继承了原型上的方法,在实例上还可能是没有this._events的,所以我们需要做一下判断,如果没有我们要生成一个:

1
2
3
4
5
6
7
8
9
10
on(eventName, callback) {
if (!this._events) {
this._events = {}
}
if (this._events[eventName]) {
this._events[eventName].push(callback)
} else {
this._events[eventName] = [callback]
}
}

off和once方法实现

  • 先看off方法

通过Array.prototype.filter方法把取消订阅的方法给过滤掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @description 取消订阅
* @param {string} eventName 事件名
* @param {function} callback
*/
off(eventName, callback) {
if(this._events && this._events[eventName]){
// filter过滤掉要取消订阅的callback
this._events[eventName] = this._events[eventName].filter(fn => {
return fn !== callback
})
}
}
  • once方法

once方法注意的是只会触发一次,也就是我们要在回调函数执行后取消订阅,所以我们可以封装为一个高阶函数,将这个高阶函数作为订阅的回调函数,在这里执行真正的回调并取消订阅

这里用箭头函数是为了保证this指向没问题

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @description 只订阅一次
* @param {string} eventName
* @param {function} callback
*/
once(eventName, callback) {
const one = () => {
callback()
this.off(eventName, one)
}
this.on(eventName, one)
}
  • 避坑

上边的代码看起来好像也没什么问题了,但是我们要想一下,如果我们通过once方法订阅了事件,但是在发布事件之前我们就通过off方法取消订阅,这样会不会有问题呢?示例如下:

1
2
3
const fn = 
eventEmitter.once('eventName', fn)
eventEmitter.off('eventName', fn)

回看我们的once方法,我们对传入的callback进行了封装,最后添加到订阅列表的是一个名为one的方法,而此时我们传入fn方法取消订阅,肯定不能成功的把这个回调函数给过滤掉,所以我们需要在once方法中把传入的回调也收集起来。

once方法修改如下:

1
2
3
4
5
6
7
8
9
once(eventName, callback) {
const one = () => {
callback()
this.off(eventName, one)
}
// 如果回调还没触发之前就像调用off方法取消订阅,我们收集到_events中的是one,one与callback肯定不相等会导致不能解绑,所以我们把callback挂载到one的自定义属性上,在off方法中做判断
+ one.l = callback;
this.on(eventName, one)
}

相应的,我们在off方法中也要添加判断:

1
2
3
4
5
6
7
8
off(eventName, callback) {
if (this._events && this._events[eventName]) {
// filter过滤掉要取消订阅的callback
this._events[eventName] = this._events[eventName].filter(fn => {
+ return fn !== callback && fn.l !== callback
})
}
}

完整手写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class EventEmitter {
constructor() {
// 用于收集订阅的事件和对应回调函数
this._events = {}
}
/**
* @description 订阅事件
* @param {string} eventName 事件名
* @param {function} callback 回调函数
*/
on(eventName, callback) {
if (!this._events) {
this._events = {}
}
if (this._events[eventName]) {
this._events[eventName].push(callback)
} else {
this._events[eventName] = [callback]
}
}
/**
* @description 发布事件
* @param {string} eventName 事件名
* @param {*} args 其他参数
*/
emit(eventName, ...args) {
// 找到对应事件的处理函数以此调用
this._events[eventName].forEach(fn => {
fn(...args)
})
}
/**
* @description 取消订阅
* @param {string} eventName 事件名
* @param {function} callback
*/
off(eventName, callback) {
if (this._events && this._events[eventName]) {
// filter过滤掉要取消订阅的callback
this._events[eventName] = this._events[eventName].filter(fn => {
return fn !== callback && fn.l !== callback
})
}
}
/**
* @description 只订阅一次
* @param {string} eventName
* @param {function} callback
*/
once(eventName, callback) {
const one = () => {
callback()
this.off(eventName, one)
}
// 如果回调还没触发之前就像调用off方法取消订阅,我们收集到_events中的是one,one与callback肯定不相等会导致不能解绑,所以我们把callback挂载到one的自定义属性上,在off方法中做判断
one.l = callback;
this.on(eventName, one)
}
}
作者

胡兆磊

发布于

2022-10-14

更新于

2022-10-23

许可协议