事件循环机制
事件循环 EventLoop
为什么会有事件循环
由于 JavaScript 是单线程语言(即程序运行时只有一个线程存在,同一时间只能做一件事),单线程会有线程运行阻塞问题(同步代码执行时,会阻塞后续代码的执行),为了解决这一问题,JavaScript 便采用了事件循环 EventLoop 机制。
为什么不设计成多线程
既然单线程有运行阻塞问题,为什么 JavaScript 不设计成多线程呢?
设计之初 JavaScript 作为一门浏览器脚本语言,通常用于操作 DOM,若设计成多线程,则会带来很复杂的同步问题,比如:一个线程要删除某 DOM 元素,另一个要添加这个 DOM 元素,此时的浏览器就不知道该怎么办了!
什么是事件循环
事件循环是一种机制,它允许 JavaScript 在单线程环境中实现异步编程。事件循环不断地从任务队列中取出任务并执行,直到任务队列清空。
- 同步代码:指按照代码的书写顺序一步一步执行的代码,会阻塞后续代码。
- 异步任务:不进入主线程,进入事件队列的任务(某个异步任务可以执行了,该任务才会进入主线程执行)。异步任务又分为微任务和宏任务:
- 微任务(
Microtask
):Promise
、process.nextTick
、MutationObserver
、异步函数(async/await) - 宏任务(
Macrotask
):setTimeout
、setInterval
、setImmediate
、requestAnimationFrame
、I/O
、UI rendering
- 微任务(
执行顺序:同步代码 > 微任务 > 宏任务
事件循环的工作流程:
- 执行同步代码,这些代码会立即进入调用栈并执行。
- 当调用栈为空时,检查微任务队列。如果有微任务,则依次执行所有微任务。
- 微任务队列清空后,从宏任务队列中取出一个任务执行。
- 重复步骤 2 和 3,直到所有任务都执行完毕。
示例
js
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('Promise1')
resolve()
}).then(() => {
console.log('Promise2')
})
console.log('script end')
这段代码的输出顺序如下:
- "script start"
- "async1 start"
- "async2"
- "Promise1"
- "script end"
- "async1 end"
- "Promise2"
- "setTimeout"
Preview
解释一下执行过程:
首先执行同步代码,输出 "script start"。
遇到
setTimeout
,将其回调函数放入宏任务队列。调用
async1()
:- 输出 "async1 start"
- 调用
async2()
,输出 "async2" await
后的代码被放入微任务队列
执行
new Promise
:- 输出 "Promise1"
resolve()
被调用,.then
的回调被放入微任务队列
输出 "script end",同步代码执行完毕。
开始执行微任务队列:
- 首先执行
async1
中await
后的代码,输出 "async1 end" - 然后执行 Promise 的
.then
,输出 "Promise2"
- 首先执行
微任务队列清空后,执行宏任务队列中的
setTimeout
回调,输出 "setTimeout"
这个输出顺序体现了 JavaScript 的事件循环机制,包括同步代码执行、微任务和宏任务的处理顺序。异步函数(async/await)和 Promise 的处理属于微任务,而 setTimeout
属于宏任务。微任务总是在当前任务执行结束后、下一个宏任务开始前执行。