用JavaScript模拟事件循环

本文属于原创文章,转载请注明–来自桃源小盼的博客

哇叽哇叽

对于很多概念性的原理,可能三两句话就能概括,但必然损失了很多细节。而实际的代码呢,无法忽略细节,最多是简化一些。

那么就让我们一起来用代码来模拟事件循环机制吧。

Talk is cheap. Show me the code.

说起来容易做起来难,历史上的马谡可能是最佳反面代表人物了。

为什么是事件循环机制,而不是别的机制?

js主线程要做各种类型的任务,例如:dom事件、布局计算、js任务、用户输入、动画、定时器。

如何解决未来的新任务?

各种事件不可能是同一时间执行,会在未来产生新的事件,所以就需要有一个机制像前台接待员一样,一直守在那里,时刻检测是否有新任务了,一有新任务就执行它,这就是事件循环机制。

1
2
3
while(true) {
doSomething()
}

如何解决积攒的新任务?

新任务太多了,前台接待员无法同时处理多个任务,只能让大家排队了,这就是任务队列机制。

为什么无法同时处理多个任务?因为js(渲染进程的主线程)是单线程执行模式。

队列是先进先出的数据结构,在js中可以理解为数组。

1
2
3
4
5
6
7
8
9
10
11
12
const queue = []
const stop = false

while(true) {
const task = queue.unshift()
task()

// 退出标志
if (stop) {
break
}
}

高优先级任务被阻塞了

如果只有一个消息队列,那么高优先级的任务一直在等待,可能会产生页面卡顿。
所以按照任务的类型分了几种队列。优先级依次向下。

  • 用户交互
  • 合成页面
  • 默认(资源加载、定时器等)
  • 空闲(垃圾回收等)
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
class Queue {
handleQueue = [] // 交互队列
composeQueue = [] // 合成队列
baseQueue = [] // 默认队列
freeQueue = [] // 空闲队列

// 插入新任务
add(task, type) {
if (type === 'handle') {
this.handleQueue.push(task)
} else if (type === 'compose') {
this.composeQueue.push(task)
} else if (type === 'base') {
this.baseQueue.push(task)
} else if (type === 'free') {
this.freeQueue.push(task)
}
}

// 获取一个任务
get() {
const queue = []
if (handleQueue.length > 0) {
queue = handleQueue
} else if (composeQueue.length > 0) {
queue = composeQueue
} else if (baseQueue.length > 0) {
queue = baseQueue
} else if (freeQueue.length > 0) {
queue = freeQueue
}
return queue.unshift()
}
}

const queue = new Queue()
const stop = false

while(true) {
const task = queue.get()
task()

// 退出标志
if (stop) {
break
}
}

页面在不同阶段,高优目标是不同的

页面在加载阶段,第一目标是先把页面渲染出来。
页面在交互阶段,第一目标是及时响应用户的操作。

为了满足不同阶段的目标,需要调整不同阶段任务队列的优先级。

natapp1

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
class Queue {
handleQueue = []
composeQueue = []
baseQueue = []
freeQueue = []
priority = []
// 设置优先级
setPriority(lifecycle) {
if (lifecycle === 'pageload') { // 页面加载
this.priority = ['baseQueue', 'handleQueue', 'composeQueue', 'freeQueue']
}
else if (lifecycle === 'handle') { // 交互阶段
this.priority = ['handleQueue', 'composeQueue', 'baseQueue', 'freeQueue']
} else if (lifecycle === 'free') { // 空闲阶段
this.priority = ['baseQueue', 'handleQueue', 'freeQueue', 'composeQueue']
}
}

get() {
const curr = []
// 根据优先级顺序来获取任务
this.priority.forEach(priority => {
const queue = this[priority]
if (queue.length > 0) {
return queue.unshift()
}
})
}
// 省略
add(task, type) {}
}

const queue = new Queue()
const stop = false

queue.setPriority('pageload')

while(true) {
const task = queue.get()
task()

// 退出标志
if (stop) {
break
}
}

如何在渲染前做一些任务?

有时候我们想在当前任务完成前再紧接着做一些任务,但是如果插入到队伍末尾,那么需要的时间可能长,可能短,这就无法稳定地按照预期来做了。

所以增加了微任务队列,在当前任务即将完成时,再执行一些事情,不用等太久。

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
class Task {
microQueue = []
// 执行任务
do() {
// start doSomething
// doSomething
// end doSomething
// 检查微任务队列
if (microQueue.length > 0) {
microQueue.forEach(microTask => microTask())
}
}
// 添加微任务
addMicro(microTask) {
this.microQueue(microTask)
}
}

// 省略,同上
class Queue {
add(task, type) {}

get() {}

setPriority(lifecycle) {}
}

const queue = new Queue()
queue.add(new Task(), 'base')

while(true) {
const task = queue.get()
task.do()

// 退出标志
if (stop) {
break
}
}

低级任务饿死现象

一直在执行高优任务,低级任务就会出现饿死现象,所以连续执行一定数量的高优任务后,需要执行一次低级任务。

异步回调

这里先说一个常识,js虽然是单线程执行,但是浏览器却是多进程的。

一个异步任务,可能是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。

setTimeout实现机制有何不同之处?

由于存在时间的概念,并不能直接放入消息队列中。浏览器又增加了一个延迟队列,还有其他的一些延迟任务都在这里执行。每次执行完消息队列中的一个任务,就要检查一遍延迟队列。

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
const delayQueue = []

// 检查延迟队列中的任务,是否到时间了
function checkDelayQueue () {
delayQueue.map(task => {
if ('到期了') {
task()
}
})
}

// 省略
class Queue {}

const queue = new Queue()

while(true) {
const task = queue.get()
task.do()

checkDelayQueue()

// 退出标志
if (stop) {
break
}
}

结尾

以上代码不是实际的浏览器实现,只是为了更好理解事件循环机制提供帮助。

希望你也写出自己的实现版本。

参考

  1. 《浏览器工作原理与实践》
  2. 《JavaScript忍者秘籍》