本文属于原创文章,转载请注明–来自桃源小盼的博客
哇叽哇叽
对于很多概念性的原理,可能三两句话就能概括,但必然损失了很多细节。而实际的代码呢,无法忽略细节,最多是简化一些。
那么就让我们一起来用伪代码来模拟事件循环机制吧。
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 } }
|
页面在不同阶段,高优目标是不同的
页面在加载阶段,第一目标是先把页面渲染出来。
页面在交互阶段,第一目标是及时响应用户的操作。
为了满足不同阶段的目标,需要调整不同阶段任务队列的优先级。

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() { 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 } }
|
结尾
以上代码不是实际的浏览器实现,只是为了更好理解事件循环机制提供帮助。
希望你也写出自己的实现版本。
参考
- 《浏览器工作原理与实践》
- 《JavaScript忍者秘籍》