JavaScript 的事件循环 (Event Loop ) 是其实现异步编程的核心机制,即使 JS 是单线程语言,它也能高效处理 I/O、网络请求、计时器等非阻塞操作。以下是其工作原理的精要解析:
核心概念
-
单线程执行
- JS 引擎(如 V8)只能顺序执行一个任务。
- 异步行为需要靠宿主环境(浏览器/Node.js)提供的事件循环调度。
-
任务队列 (Task Queue)
- 所有异步操作完成后对应的回调函数会进入队列等待执行。
- 队列类型包括:
- 宏任务队列 (Macrotask Queue) :
script
(整体代码)、setTimeout
、setInterval
、DOM 事件回调、I/O 操作。 - 微任务队列 (Microtask Queue) :
Promise.then()
、await
后续代码、MutationObserver
、queueMicrotask()
。
- 宏任务队列 (Macrotask Queue) :
事件循环执行流程
-
执行同步代码
运行主线程的同步任务,形成执行栈(Call Stack),若遇到异步操作(如
setTimeout
或fetch
),交给 Web API 处理。 -
异步任务回调入队
异步操作完成后,Web API 将其回调推入对应队列:
- 宏任务回调 → 宏任务队列
- 微任务回调 → 微任务队列
-
轮询事件队列(关键步骤)
- 执行完当前所有同步任务 后,按优先级处理队列:
- 清空微任务队列
将所有可执行的微任务按顺序执行直至队列为空(执行中可能产生新微任务)。 - 执行一个宏任务
从宏任务队列中取出最早的一个任务执行。 - 再次清空微任务队列
执行该宏任务过程中产生的所有微任务。 - 更新渲染(浏览器环境下)
如有需要,执行 UI 渲染、布局计算等。
- 清空微任务队列
- 循环:重复步骤 3,直至所有队列为空。
- 执行完当前所有同步任务 后,按优先级处理队列:
执行顺序示例代码解析
javascript
console.log("1. 同步代码开始");
setTimeout(() => {
console.log("4. 宏任务 (setTimeout)");
}, 0);
Promise.resolve().then(() => {
console.log("3. 微任务 (Promise)");
});
console.log("2. 同步代码结束");
输出顺序 :
1. 同步代码开始
→ 2. 同步代码结束
→ 3. 微任务 (Promise)
→ 4. 宏任务 (setTimeout)
原因:
- 先执行完所有同步代码(输出 1 和 2)。
- 微任务队列优先于宏任务队列执行(
Promise.then()
在setTimeout
前)。
为什么区分宏任务与微任务?
- 微任务的优先级更高
确保诸如 Promise 状态更新后能立即触发回调,提升用户体验(如快速响应用户操作)。 - 避免渲染阻塞
浏览器在宏任务之间插入 UI 渲染流程,而微任务在渲染前全部执行,保证页面及时更新。
事件循环流程图
同步代码执行
↓
遇到异步操作 → Web API 处理 → 完成后回调入队
↓ ↗ 宏任务队列
| /
事件循环开始 ↓
| 下一轮循环 取出最早宏任务 → 执行
| ↑ ↓
|------→ 清空微任务队列 → 可能产生新微任务
|
(浏览器环境下)
↓
需要渲染? → 执行渲染流程
⚠️ 常见误区
setTimeout(0)
并非立即执行
定时器时间仅表示最小延迟,实际回调需等待同步任务及队列中的任务完成。- 微任务会阻塞页面渲染
长时间的微任务链会导致 UI 冻结。 - Node.js 与浏览器的事件循环差异
Node.js 的事件循环分为更多阶段(如nextTick
优先级最高)。
掌握事件循环机制,能帮你精准控制代码时序、避免竞态条件,并理解框架如 Vue/React 的底层调度原理!