JavaScript
事件循环
JavaScript 是单线程语言,同一时刻只能执行一件事。事件循环(Event Loop)是浏览器和 Node.js 协调异步任务的核心机制,它决定了代码的实际执行顺序。
一、浏览器中的事件循环
执行模型
JavaScript 将任务分为两类:
- 同步任务:进入主调用栈,立即按顺序执行。
- 异步任务:交给浏览器 API 处理(定时器、网络请求、DOM 事件等),完成后将回调放入对应队列,等待执行。
两类任务队列
| 类型 | 描述 | 常见来源 |
|---|---|---|
| 宏任务(Macro Task) | 主体任务,每次事件循环执行一个 | setTimeout、setInterval、UI 事件、MessageChannel |
| 微任务(Micro Task) | 每个宏任务结束后立即全部清空,优先级更高 | Promise.then、queueMicrotask、MutationObserver |
执行顺序
执行同步代码(调用栈)
↓
清空全部微任务队列
↓
执行一个宏任务
↓
清空全部微任务队列
↓
执行下一个宏任务 ...(如此循环)示例
console.log('同步 1')
setTimeout(() => {
console.log('宏任务 1')
Promise.resolve().then(() => {
console.log('宏任务 1 产生的微任务')
})
}, 0)
Promise.resolve().then(() => {
console.log('微任务 1')
setTimeout(() => {
console.log('微任务产生的宏任务')
}, 0)
})
console.log('同步 2')
// 输出顺序:
// 同步 1
// 同步 2
// 微任务 1
// 宏任务 1
// 宏任务 1 产生的微任务
// 微任务产生的宏任务二、Node.js 中的事件循环
Node.js 的事件循环基于 libuv,分为六个阶段,依次循环执行:
| 阶段 | 说明 |
|---|---|
| timers | 执行 setTimeout / setInterval 到期的回调 |
| pending callbacks | 执行上一轮延迟的 I/O 错误回调 |
| idle / prepare | 内部使用 |
| poll | 获取新的 I/O 事件,执行 I/O 相关回调 |
| check | 执行 setImmediate 回调 |
| close callbacks | 执行关闭事件回调,如 socket.on('close', ...) |
Node.js 还提供了 process.nextTick(),它的优先级高于所有微任务,在当前操作完成后、进入下一阶段前立即执行:
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
setTimeout(() => console.log('setTimeout'), 0)
// 输出顺序:
// nextTick
// Promise
// setTimeout在 I/O 回调内部,setImmediate 总是比 setTimeout 先执行:
const fs = require('fs')
fs.readFile('./file.txt', () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
// 在 I/O 回调中输出顺序:
// immediate
// timeout三、浏览器与 Node.js 的区别
| 对比点 | 浏览器 | Node.js |
|---|---|---|
| 微任务清空时机 | 每个宏任务执行完后立即清空 | 每个阶段结束后清空 |
| 最高优先级异步 | queueMicrotask / Promise.then | process.nextTick |
| 额外 API | requestAnimationFrame | setImmediate、process.nextTick |
// 浏览器:Promise 微任务先于 setTimeout 宏任务
Promise.resolve().then(() => console.log('Promise'))
setTimeout(() => console.log('setTimeout'), 0)
// 输出:Promise → setTimeout
// Node.js:process.nextTick 优先级最高
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
// 输出:nextTick → Promise四、定时器的调度机制
每一轮事件循环中,浏览器检查 setTimeout 注册时间加上 delay 是否已到期,到期才将其回调放入宏任务队列。delay 为 0 并不代表"立即执行",而是"尽早执行"。
const fakeAjax = (delay = 1000) =>
new Promise(resolve => {
setTimeout(() => {
console.log('sleep', delay)
resolve()
}, delay)
})
setTimeout(() => console.log('setTimeout 0'), 0)
fakeAjax() // delay 1000ms
setTimeout(() => console.log('setTimeout 100'), 100)
// 输出顺序:
// setTimeout 0 (最早到期)
// setTimeout 100
// sleep 1000