JavaScript 的事件循环(Event Loop)是其处理异步任务的核心机制。由于 JavaScript 是单线程的,事件循环通过任务队列和循环调度实现了非阻塞的异步行为。以下是事件循环的核心概念和工作流程:
核心概念
-
调用栈(Call Stack)
- 用于跟踪当前执行的代码(函数调用),后进先出(LIFO)。
- 同步任务会直接压入栈中执行,遇到异步任务时,会将其交给浏览器 API 处理。
-
任务队列(Task Queue)
-
异步任务完成后,其回调函数会被推入队列中。
-
分为两种队列:
- 宏任务队列(MacroTask Queue) :如
setTimeout
、setInterval
、DOM 事件回调、I/O 操作等。 - 微任务队列(MicroTask Queue) :如
Promise.then
、MutationObserver
、queueMicrotask
等。
- 宏任务队列(MacroTask Queue) :如
-
-
事件循环的工作流程
- 每次循环(一次 Tick)会依次处理宏任务、微任务,直到所有队列清空。
事件循环的步骤
-
执行一个宏任务(从宏任务队列中取出最早的任务,如
script
标签整体代码)。 -
执行所有微任务:
- 清空微任务队列中的所有任务(包括执行过程中新产生的微任务)。
-
渲染页面(如果需要):
- 执行 UI 渲染、布局、绘制等操作(浏览器可能会跳过此步骤以优化性能)。
-
开始下一轮循环,处理下一个宏任务。
执行顺序规则
-
微任务优先级高于宏任务:
- 每次宏任务执行完毕后,会立即清空微任务队列。
-
同类型任务的顺序:
- 宏任务按队列顺序执行,微任务按队列顺序执行。
console.log("Start"); // 同步任务,直接执行
setTimeout(() => {
console.log("setTimeout"); // 宏任务
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1"); // 微任务
})
.then(() => {
console.log("Promise 2"); // 微任务
});
console.log("End"); // 同步任务,直接执行
输出顺序:
Start → End → Promise 1 → Promise 2 → setTimeout
解释:
- 同步代码依次执行,输出
Start
和End
。 - 执行微任务队列中的
Promise 1
和Promise 2
。 - 最后执行宏任务队列中的
setTimeout
。
常见异步任务类型
任务类型 | 示例 |
---|---|
宏任务 | setTimeout , setInterval , requestAnimationFrame , I/O 操作, UI 渲染 |
微任务 | Promise.then , MutationObserver , queueMicrotask |
关键总结
- 同步任务优先执行,异步任务通过队列调度。
- 微任务在每轮事件循环的末尾立即执行,宏任务在下一轮循环处理。
- 避免阻塞事件循环:长时间运行的同步代码会延迟页面渲染和任务处理。
掌握事件循环机制,可以更精准地控制异步代码的执行顺序,避免常见的陷阱(如 setTimeout
延迟不精确)。