事件循环是如何产生的
在浏览器中,JS 的主线程是单线程的,异步的代码被放到了任务队列中,JS 主线程不断地轮询任务队列,每次从队首取出一个任务并进行处理,从而构成了事件环。
任务队列的类型
任务队列有两种,分别是宏任务(macrotask)队列和微任务(microtask)队列,用于存放不同的任务:
- 宏任务主要有:script(用户代码)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel 等
- 微任务主要有:Promise.then、 MutationObserver 等
事件环的工作流程
- JS 引擎首先从上而下执行用户 script 代码(宏任务)
- 如果遇到 setTimeout、promise、click事件等,按照分类放入宏任务队列或微任务队列中
- 继续执行后面的代码,script 执行完毕后,会清空所有的微任务
- 微任务清空后可能会进行一次 UI 渲染(不是每次都会)
- 然后再去宏任务队列中取出一个执行,循环往复
图示
备注
其实在浏览器中,宏任务队列是可以有多个的,微任务队列只能有一个,上面的图把所有的宏任务都放到一个队列当中并不严谨,但是定时器、事件和 ajax 类型的宏任务是放到一个宏任务队列中的。规范 里面的原话是这么说的:
An event loop has one or more task queues. A task queue is a set of tasks.
这里盗用一张图:
如果出现多个宏任务队列,应该如何取宏任务呢?这个是比较灵活的,完全交给浏览器自己来实现,规范里面不做限制,但是只要保证下面两点即可:
- 相同来源的宏任务都放到一个宏任务队列中
- 每个宏任务队列都是按照先进先出的顺序取出
规范里面也给出了说明:
For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.
事件循环的逻辑可以用下面的代码来描述:
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue() // 选择宏任务队列
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask() // 取出宏任务并执行
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) { // 清空微任务队列
microtaskQueue.processNextMicrotask()
}
if (shouldRender()) { // 浏览器决定是否渲染,如果是,则执行下面的逻辑
applyScrollResizeAndCSS()
runAnimationFrames()
render()
}
}
图示如下: