跳到主要内容

事件循环和任务队列

前端的 Event Loop(事件循环) 是 JavaScript 实现异步编程的核心机制。

这是因为现代浏览器中的 JavaScript 引擎(如 V8)是单线程的,但是 Web 应用需要处理大量异步操作(比如网络请求、用户交互、定时器等),这时就通过事件循环(Event Loop) 和 任务队列(Task Queues) 的机制,高效地处理异步任务,而不会阻塞主线程。

✅ 核心思想:同步代码立即执行,异步代码稍后执行(由 Event Loop 调度)


一、基本组成

1. 调用栈(Call Stack)

  • JavaScript 代码的执行是基于调用栈的。
  • 每当函数被调用,就压入栈;执行完后弹出。
  • 如果栈溢出(如无限递归),就会报错。

2. Web APIs(浏览器提供的异步能力)

  • 浏览器提供了一组非 JS 引擎原生的功能,如:
    • setTimeout
    • fetch / 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

解释:

  1. 主脚本本身是一个宏任务,先执行:
    • 打印 A
    • setTimeout 注册回调到宏任务队列(稍后执行)
    • Promise.then 注册回调到微任务队列
    • 打印 D
  2. 宏任务执行完毕,开始处理微任务队列 → 打印 C
  3. 下一轮事件循环,取宏任务队列中的 setTimeout 回调 → 打印 B

四、宏任务队列和微任务队列都是在 js 线程上吗

这是一个非常关键且常见的问题。答案是:

宏任务队列和微任务队列本身并不“运行”在 JavaScript 线程上,但它们的回调函数(即任务)是在 JavaScript 主线程(JS 线程)上执行的。

JavaScript 是单线程的,所有 同步代码、宏任务回调、微任务回调 最终都必须在这个线程上执行。


JavaScript 是单线程的

  • JavaScript 是单线程的,所有 同步代码、宏任务回调、微任务回调 最终都必须在这个线程上执行。

任务队列 ≠ 执行线程

✅ 队列是数据结构,不是执行环境

  • 宏任务队列(Macrotask Queue)微任务队列(Microtask Queue) 只是浏览器维护的两个 FIFO(先进先出)的数据结构,用来存放待执行的回调函数。
  • 它们本身不“运行”代码,只是排队等待被事件循环调度

✅ 回调的执行一定发生在 JS 主线程

  • 当事件循环决定处理一个宏任务或微任务时,它会把对应的回调函数压入 JS 调用栈
  • 此时,该回调才真正在 JS 主线程上执行

异步操作的“后台”在哪里?

虽然 JS 是单线程的,但浏览器是多线程的。例如:

异步操作后台线程/模块
setTimeout浏览器定时器线程
网络请求(fetch)网络线程(Network Thread)
DOM 事件UI 线程(处理点击、滚动等)
PromiseJS 引擎内部(但回调仍回主线程)

这些后台线程完成工作后,会将回调函数推入相应的任务队列(宏 or 微),然后由事件循环在 JS 主线程空闲时调度执行

🔑 关键点:所有 JS 回调最终都在同一个主线程执行,但触发这些回调的异步操作可能发生在其他线程。