事件循环和任务队列
前端的 Event Loop(事件循环) 是 JavaScript 实现异步编程的核心机制。
这是因为现代浏览器中的 JavaScript 引擎(如 V8)是单线程的,但是 Web 应用需要处理大量异步操作(比如网络请求、用户交互、定时器等),这时就通过事件循环(Event Loop) 和 任务队列(Task Queues) 的机制,高效地处理异步任务,而不会阻塞主线程。
✅ 核心思想:同步代码立即执行,异步代码稍后执行(由 Event Loop 调度)。
一、基本组成
1. 调用栈(Call Stack)
- JavaScript 代码的执行是基于调用栈的。
- 每当函数被调用,就压入栈;执行完后弹出。
- 如果栈溢出(如无限递归),就会报错。
2. Web APIs(浏览器提供的异步能力)
- 浏览器提供了一组非 JS 引擎原生的功能,如:
setTimeoutfetch/XMLHttpRequest- DOM 事件(click, scroll 等)
- Promise(部分由引擎实现,但其回调调度依赖微任务队列)
- 这些 API 是多线程的,它们在后台运行,不阻塞主线程。
3. 任务队列(Task Queues)
浏览器维护多个任务队列,主要分为两类:
(1) 宏任务队列(Macrotask Queue)
也叫 Task Queue,包含以下类型任务:
setTimeout/setInterval回调- I/O 操作(如网络响应)
- 用户交互事件(如 click、keypress)
requestAnimationFrame(在某些实现中被视为特殊宏任务)<script>标签的解析和执行(初始 script 也算一个宏任务)
每次事件循环只处理一个宏任务。
(2) 微任务队列(Microtask Queue)
优先级高于宏任务,包括:
Promise.then/catch/finally回调queueMicrotask()MutationObserver回调(用于监听 DOM 变化)
在每个宏任务执行完毕后,会清空整个微任务队列(即执行所有当前存在的微任务)。
二、事件循环(Event Loop)工作流程
事件循环是一个持续运行的机制,负责协调调用栈、任务队列和 Web APIs。其基本流程如下:
while (true) {
1. 从宏任务队列取出一个任务(如果有),执行它(压入调用栈)。
2. 执行过程中,可能触发 Web API(如 setTimeout),这些 API 在后台运行。
3. 当宏任务执行完毕,检查微任务队列:
→ 执行所有微任务(直到队列为空)。
→ 注意:微任务中新增的微任务也会被立即执行(因为是“清空”队列)。
4. 渲染阶段(可选):
→ 浏览器可能在此时更新 UI(如重排、重绘),但不是每次循环都渲染。
→ 通常每 16ms(60fps)渲染一次,受 `requestAnimationFrame` 影响。
5. 回到步骤 1,继续下一个宏任务。
}
⚠️ 注意:微任务是在当前宏任务结束后、下一个宏任务开始前全部执行完的。
三、示例分析
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
输出顺序:A → D → C → B
解释:
- 主脚本本身是一个宏任务,先执行:
- 打印 A
setTimeout注册回调到宏任务队列(稍后执行)Promise.then注册回调到微任务队列- 打印 D
- 宏任务执行完毕,开始处理微任务队列 → 打印 C
- 下一轮事件循环,取宏任务队列中的
setTimeout回调 → 打印 B
四、宏任务队列和微任务队列都是在 js 线程上吗
这是一个非常关键且常见的问题。答案是:
宏任务队列和微任务队列本身并不“运行”在 JavaScript 线程上,但它们的回调函数(即任务)是在 JavaScript 主线程(JS 线程)上执行的。
JavaScript 是单线程的,所有 同步代码、宏任务回调、微任务回调 最终都必须在这个线程上执行。
JavaScript 是单线程的
- JavaScript 是单线程的,所有 同步代码、宏任务回调、微任务回调 最终都必须在这个线程上执行。
任务队列 ≠ 执行线程
✅ 队列是数据结构,不是执行环境
- 宏任务队列(Macrotask Queue) 和 微任务队列(Microtask Queue) 只是浏览器维护的两个 FIFO(先进先出)的数据结构,用来存放待执行的回调函数。
- 它们本身不“运行”代码,只是排队等待被事件循环调度。
✅ 回调的执行一定发生在 JS 主线程
- 当事件循环决定处理一个宏任务或微任务时,它会把对应的回调函数压入 JS 调用栈。
- 此时,该回调才真正在 JS 主线程上执行。
异步操作的“后台”在哪里?
虽然 JS 是单线程的,但浏览器是多线程的。例如:
| 异步操作 | 后台线程/模块 |
|---|---|
setTimeout | 浏览器定时器线程 |
| 网络请求(fetch) | 网络线程(Network Thread) |
| DOM 事件 | UI 线程(处理点击、滚动等) |
| Promise | JS 引擎内部(但回调仍回主线程) |
这些后台线程完成工作后,会将回调函数推入相应的任务队列(宏 or 微),然后由事件循环在 JS 主线程空闲时调度执行。
🔑 关键点:所有 JS 回调最终都在同一个主线程执行,但触发这些回调的异步操作可能发生在其他线程。