JavaScript 事件循环
事件循环的机制:调用栈、任务队列、微任务队列、requestAnimationFrame,以及浏览器与 Node.js 的差异。
#tech / dev / frontend
#resource / js
#type / concept
#status / growing
[!info] related notes
- 所属 MOC: [[js-moc|JavaScript MOC]]
- 相关概念: [[dom]], web-worker, 调用栈和执行上下文的关系
JavaScript 事件循环
定义
事件循环(Event Loop)是 JavaScript 运行时的核心机制,负责协调调用栈、任务队列和微任务队列,确保单线程的 JS 能够非阻塞地处理异步操作。
机制
浏览器中事件循环的每次迭代(tick):
- 从宏任务队列取出一个任务执行
- 执行所有微任务队列中的任务(直到清空)
- 如果需要渲染,执行渲染步骤(布局、绘制)
- 回到步骤 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/setIntervalsetImmediate(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/finallyMutationObserverqueueMicrotask()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/O | setTimeout, setInterval, setImmediate, I/O |
| 微任务 | Promise, MutationObserver, queueMicrotask | Promise, 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 在绘制前执行。记住:同步 -> 微任务 -> 渲染 -> 宏任务。
Related notes
- [[js-moc|JavaScript MOC]]
- [[dom]]
- web-worker
- 调用栈和执行上下文的关系