本文属于原创文章,转载请注明--来自桃源小盼的博客
哇叽哇叽
对于很多概念性的原理,可能三两句话就能概括,但必然损失了很多细节。而实际的代码呢,无法忽略细节,最多是简化一些。
那么就让我们一起来用伪代码来模拟事件循环机制吧。
Talk is cheap. Show me the code.
说起来容易做起来难,历史上的马谡可能是最佳反面代表人物了。
为什么是事件循环机制,而不是别的机制?
js主线程要做各种类型的任务,例如:dom事件、布局计算、js任务、用户输入、动画、定时器。
如何解决未来的新任务?
各种事件不可能是同一时间执行,会在未来产生新的事件,所以就需要有一个机制像前台接待员一样,一直守在那里,时刻检测是否有新任务了,一有新任务就执行它,这就是事件循环机制。
while(true) {
doSomething()
}
如何解决积攒的新任务?
新任务太多了,前台接待员无法同时处理多个任务,只能让大家排队了,这就是任务队列机制。
为什么无法同时处理多个任务?因为js(渲染进程的主线程)是单线程执行模式。
队列是先进先出的数据结构,在js中可以理解为数组。
const queue = []
const stop = false
while(true) {
const task = queue.unshift()
task()
// 退出标志
if (stop) {
break
}
}
高优先级任务被阻塞了
如果只有一个消息队列,那么高优先级的任务一直在等待,可能会产生页面卡顿。
所以按照任务的类型分了几种队列。优先级依次向下。
- 用户交互
- 合成页面
- 默认(资源加载、定时器等)
- 空闲(垃圾回收等)
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
}
}
页面在不同阶段,高优目标是不同的
页面在加载阶段,第一目标是先把页面渲染出来。
页面在交互阶段,第一目标是及时响应用户的操作。
为了满足不同阶段的目标,需要调整不同阶段任务队列的优先级。
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
}
}
如何在渲染前做一些任务?
有时候我们想在当前任务完成前再紧接着做一些任务,但是如果插入到队伍末尾,那么需要的时间可能长,可能短,这就无法稳定地按照预期来做了。
所以增加了微任务队列,在当前任务即将完成时,再执行一些事情,不用等太久。
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实现机制有何不同之处?
由于存在时间的概念,并不能直接放入消息队列中。浏览器又增加了一个延迟队列,还有其他的一些延迟任务都在这里执行。每次执行完消息队列中的一个任务,就要检查一遍延迟队列。
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
}
}
结尾
以上代码不是实际的浏览器实现,只是为了更好理解事件循环机制提供帮助。
希望你也写出自己的实现版本。
参考
- 《浏览器工作原理与实践》
- 《JavaScript忍者秘籍》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。