Roll your own EventEmitter

[Musings]
今天又是下雨的一天,写完这个 post 就去花园里把自动浇水系统搭了。拖了一周没搞了。😓

今天是手写 EventEmitter 的一天,边写边改,一个版本一个版本的迭代,主要就是熟练下一些基础的结构,从头写一遍,下面就开始正片了

第一个版本主要就是实现 on 和 emit 函数

here comes the first version of on() and emit() function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
console.log("*".repeat(40), "version 1");
class EventEmitter {
private eventRegister: Map<string, Function>;
constructor() {
this.eventRegister = new Map();
}
on(event: string, fn: Function) {
this.eventRegister.set(event, fn);
}
emit(event: string, info: string) {
this.eventRegister.get(event)!(info);
}
}

let bus = new EventEmitter();
bus.on("click", (val) => console.log("you clicked!✅ and say", val));
bus.emit("click", "Yaaa");

bus.on("touch", (val) => console.log("you touch!✅ and say", val));
bus.emit("touch", "Humm");
bus.emit("click", "Yaaa");
bus.emit("touch", "Humm");

first step is to think about the data structure. The most straightforward choice is to use Map, because we need to use a <K,V> for storage

here terminal will output with:

now, the first issue comes, when we use Map<event,Function> as the data structure then we will face how to trigger the same event with different callback function… problems like below.

1
2
3
bus.on("click", () => console.log("click1"));
bus.on("click", () => console.log("click2"));
bus.emit("click", "hi");

the second log will overlap the first one,because if there is existed key in Map, then set() will update with the value and overlap the previous valu.
so Map<K,V>should use -> Map<string, Function[]> instead of Map<string, Function>

then we will naturally think of below version 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
console.log("*".repeat(40), "version 2");
class EventEmitterV2 {
private eventRegister: Map<string, Function[]>;
constructor() {
this.eventRegister = new Map();
}
on(event: string, fn: Function) {
const fnList = this.eventRegister.get(event);
if (fnList) fnList.push(fn);
else this.eventRegister.set(event, [fn]);
}
emit(event: string, info: string) {
this.eventRegister.get(event)?.forEach((fn) => fn(info));
}
}

let busV2 = new EventEmitterV2();
busV2.on("click", (val) => console.log("you clicked!✅ and say", val));
busV2.emit("click", "Yaaa");

busV2.on("click", (val) => console.log("you strong click!🚔 and say", val));
busV2.emit("click", "Yaaa");

Now it works as expected 🎉.

Next, let’s consider the off feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log("*".repeat(40), "version 3");
class EventEmitterV3 {
private eventRegister: Map<string, Function[]>;
constructor() {
this.eventRegister = new Map();
}
on(event: string, fn: Function) {
const fnList = this.eventRegister.get(event);
if (fnList) fnList.push(fn);
else this.eventRegister.set(event, [fn]);
}
emit(event: string, info: string) {
this.eventRegister.get(event)?.forEach((fn) => fn(info));
}

off(event: string, fn: Function) {
if (!this.eventRegister.get(event)) return;
const fnList = this.eventRegister.get(event)?.filter((f) => f !== fn);
this.eventRegister.set(event, fnList!);
}
}

it’s not a big deal right? The off() method simply finds the event and use use filter to rebuild the listener list.

Now let’s consider the once() method. It’s a little trickier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
console.log("*".repeat(40), "version 3");
class EventEmitterV4 {
private eventRegister: Map<string, { fn: Function, onceFlag: boolean }[]>;//we need to update the structure
constructor() {
this.eventRegister = new Map();
}

on(event: string, fn: Function, onceFlag: boolean = false) {
const fnList = this.eventRegister.get(event);
if (fnList) fnList.push({ fn, onceFlag }); // need to baesed on the structure
else this.eventRegister.set(event, [{ fn, onceFlag }]);// need to baesed on the structure
}

once(event: string, fn: Function, onceFlag: boolean = false) {
const fnList = this.eventRegister.get(event);
if (fnList) fnList.push({ fn, onceFlag });// need to baesed on the structure
else this.eventRegister.set(event, [{ fn, onceFlag }]);// need to baesed on the structure
}

emit(event: string, info: string) {... }

off(event: string, fn: Function) {... }

}

This approach is intuitive and easy to understand, but it doesn’t feel elegant. As developers, we value elegance, so let’s use a neat trick to implement it. Here’s where the magic comes in:

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
console.log("*".repeat(40), "version 4");

class EventEmitterV4 {
private eventRegister: Map<string, Function[]>
constructor() {
this.eventRegister = new Map();
}
on(event: string, fn: Function) {
const fnList = this.eventRegister.get(event);
if (fnList) fnList.push(fn);
else this.eventRegister.set(event, [fn]);
}
emit(event: string, info: string) {
this.eventRegister.get(event)?.forEach(fn => fn(info))
}

off(event: string, fn: Function) {
if (!this.eventRegister.get(event)) return;
const fnList = this.eventRegister.get(event)?.filter(f => f !== fn);
this.eventRegister.set(event, fnList!);
}

once(event: string, fn: Function) {
//nothing changed above, just to use a wrapper to implement this feature.
const wrapper = (args) => {
this.off(event, wrapper);
fn(args);
}
this.on(event, wrapper);%t

// For newcomers to TypeScript, this might feel a bit hard to understand, but think of it this way:
1. this. on(event,fn()); // this is what we want, but we need to invoke the off() after fn()
// then we create a wrapper to do it, and set the wrapper to this.on(..)
}
}

let fnV4 = (val) => console.log("only once trigger", val);
const eventEmitterV4 = new EventEmitterV4();
eventEmitterV4.once("click", fnV4);
eventEmitterV4.emit("click", "onClick");
eventEmitterV4.emit("click", "onClick");
eventEmitterV4.emit("click", "onClick");

Finally, here’s the complete log output:

BB repo url: https://github.com/XProfessorJ/code-evolution-90days/blob/main/src/02-EventEmmitter.ts