JavaScript 事件循环

事件循环的机制:调用栈、任务队列、微任务队列、requestAnimationFrame,以及浏览器与 Node.js 的差异。

#tech / dev / frontend #resource / js #type / concept #status / growing

[!info] related notes

JavaScript 事件循环

定义

事件循环(Event Loop)是 JavaScript 运行时的核心机制,负责协调调用栈、任务队列和微任务队列,确保单线程的 JS 能够非阻塞地处理异步操作。

机制

浏览器中事件循环的每次迭代(tick):

  1. 从宏任务队列取出一个任务执行
  2. 执行所有微任务队列中的任务(直到清空)
  3. 如果需要渲染,执行渲染步骤(布局、绘制)
  4. 回到步骤 1
┌───────────────────────┐
│      调用栈 (Call Stack)  │
└──────────┬────────────┘
           │ 执行完一个宏任务

┌───────────────────────┐
│   微任务队列 (Microtask)  │  ← 全部清空
└──────────┬────────────┘

┌───────────────────────┐
│     渲染 (Render)        │  ← 可能发生
└──────────┬────────────┘

┌───────────────────────┐
│   宏任务队列 (Macrotask)  │  ← 取下一个
└───────────────────────┘

调用栈

调用栈是同步代码执行的地方。函数调用时入栈,执行完毕后出栈。

function a() {
  console.log('a')
  b()
  console.log('a done')
}

function b() {
  console.log('b')
}

a()
// 调用栈: [a] -> [a, b] -> [a] -> []
// 输出: a, b, a done
// 无限递归会导致 RangeError: Maximum call stack size exceeded

宏任务(Macrotask)

常见的宏任务来源:

  • setTimeout / setInterval
  • setImmediate(Node.js)
  • I/O 操作
  • UI 渲染
  • requestAnimationFrame(严格来说在渲染前执行,有些归类为特殊宏任务)
console.log('1')               // 同步

setTimeout(() => {
  console.log('2')             // 宏任务
}, 0)

Promise.resolve().then(() => {
  console.log('3')             // 微任务
})

console.log('4')               // 同步

// 输出: 1, 4, 3, 2

微任务(Microtask)

微任务在当前宏任务结束后、下一个宏任务开始前全部执行完毕。

常见的微任务来源:

  • Promise.then / catch / finally
  • MutationObserver
  • queueMicrotask()
  • await 后面的代码(相当于 .then()
console.log('start')

setTimeout(() => console.log('timeout'), 0)

Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'))

console.log('end')

// 输出: start, end, promise1, promise2, timeout

关键点:微任务中产生的新微任务也会在当前轮次执行,不会推迟到下一轮。

Promise.resolve().then(() => {
  console.log('micro 1')
  Promise.resolve().then(() => {
    console.log('micro 2')  // 这个也在当前轮次执行
  })
})

setTimeout(() => console.log('macro'), 0)

// 输出: micro 1, micro 2, macro

async/await 的执行顺序

await 后面的代码相当于放在 .then() 中,是微任务。async function foo() { await bar(); console.log('end') } 中,console.log('end') 会在当前宏任务的微任务阶段执行。

requestAnimationFrame

requestAnimationFrame(rAF)在浏览器下一次重绘之前执行,通常在微任务之后、绘制之前。

requestAnimationFrame(() => {
  // 在下一帧绘制前执行
  element.style.transform = `translateX(${x}px)`
})

执行顺序:

宏任务 -> 微任务 -> requestAnimationFrame -> 绘制

浏览器 vs Node.js 的差异

浏览器Node.js
宏任务setTimeout, setInterval, I/OsetTimeout, setInterval, setImmediate, I/O
微任务Promise, MutationObserver, queueMicrotaskPromise, queueMicrotask, process.nextTick
特殊rAF

Node.js 的 process.nextTick 优先级高于其他微任务:

Promise.resolve().then(() => console.log('promise'))
process.nextTick(() => console.log('nextTick'))

// Node.js 输出: nextTick, promise

Node.js 事件循环分为 timers → pending callbacks → poll → check → close callbacks 等阶段,每个阶段之间都会清空微任务队列。

边界

  • setTimeout(fn, 0) 不是立即执行,而是尽快执行(最少 4ms 延迟)
  • 微任务可能阻塞渲染:如果微任务队列不断产生新微任务,浏览器无法执行渲染
  • requestAnimationFrame 不保证每帧都执行(页面不可见时可能暂停)
  • 在 Node.js 中,setImmediate 在 check 阶段执行,setTimeout(fn, 0) 在 timers 阶段执行,顺序取决于上下文

一句话记忆法

同步代码先执行,微任务在宏任务之间全部清空,rAF 在绘制前执行。记住:同步 -> 微任务 -> 渲染 -> 宏任务

创建于 2026/5/27 更新于 2026/5/27