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

Roll your own debounce / throttle

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
function debounce(fn: Function, interval: number) {
let timer: any = null;
return function () {
//之所以这里return函数,是为了让timer的作用于扩展,能保证多个function受同一个timer影响
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => fn(), interval);
};
}

//这里的逻辑是,拿到了debouce方法返回的函数引用,然后调用这个函数引用3次,那只有第一次进去的时候才会开始timer
const log = debounce(() => console.log(1), 2000);
log();
log();

//继续理解下面的逻辑,为什么log2和log1打印的时候不会共用一个timer,原因是因为下面调用debounce的时候返回的不是同一个函数引用,所以是隔离的
const log2 = debounce(() => console.log(2), 2000);
log2();
log2();
log2();

//output:
// 1
// 2

同理,继续思考throttle的写法

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
function throttle(fn: Function, interval: number): Function {
let timer: any = null;
return function () {
if (!timer) timer = setTimeout(() => {
fn();
timer = null;//这里要注意把timer最后设置为null,不然只有第一次函数会执行,再往后就不会执行了,可以注释掉这行,看第一个log的运行逻辑
}, interval)
return;
}
}

//这个log可以看到没有timer设置为null的样子
const log = throttle(() => console.log('hit'), 1000);
log(); // 1s 后打印 'hit'
setTimeout(log, 1500);


const fn = () => { console.log(1) }
const log2 = throttle(fn, 2000);
const log3 = throttle(() => console.log(2), 3000);
log2();
log2();
log3();
log3();

//output:
// hit
// 1
// hit
// 2

在debounce和throttle的case里,去理解闭包在拉长变量作用域的作用,timer锁在返回函数的作用域里,延长它的生命周期,如果没有闭包的情况下,timer会在debounce和throttle函数在被调用以后立即被销毁,在下一次调用的时候就没法拿到上一次的timer定时器的引用了
有了闭包,就等于可以记忆timer了,从而实现只执行最后一次固定时间只执行一次的效果

When we typing in Chrome

Outline

  1. DNS
  2. TCP handshake
  3. TLS connection
  4. HTTP / HTTPS
  5. TCP

DNS

When we typing domain name in chrome, then …

  1. Chrome first checks its own DNS cache.
  2. If not found, the system checks the operating system’s DNS cache and the hosts file.
  3. If still not found, the request is sent to the configured recursive resolver
  4. The recursive resolver performs the lookup step by step:
    • It asks a root server, which points to the relevant top-level domain (TLD) server (e.g., .com).
    • Then it queries the TLD server, which points to the authoritative server for the domain (e.g., example.com).
    • Finally, it queries the authoritative DNS server, which provides the actual IP address.
  5. The IP address is returned to the client, and Chrome uses it to establish the connection.

TCP handshake

  1. Client side will send api with [SYNC = 1], Seq = i,
  2. Server side will return api with [SYNC = 1, ACK = 1] , Ack = i+1, Seq = u
  3. Client side will send api with [ACK = 1], Ack = u+1

TCP handshake(by wireshark)

TLS Connection

HTTP / HTTPS

TCP disconnection

  1. Client send [FIN = 1, ACK = 1], seq = u,
  2. Server side send [ ACK = 1], Seq = j, Ack = u+1
  3. Server side send [FIN = 1, ACK = 1], Seq = j, Ack = u+1
  4. Client send [ACK = 1], Seq = x, Ack = j+1
    TCP handshake(by wireshark)