JavaScript 事件循环与并发模型
JavaScript 是单线程语言,但通过事件循环机制实现了非阻塞异步操作。理解事件循环是深入掌握 JavaScript 的关键。
JavaScript 执行模型
单线程模型
JavaScript 采用单线程执行模型,同一时刻只能执行一个任务。这避免了复杂的线程同步问题。
javascriptconsole.log('1'); setTimeout(() => { console.log('2'); }, 0); console.log('3'); // 输出顺序:1 -> 3 -> 2
为什么 JavaScript 是单线程?
- 浏览器环境: DOM 操作基于单一线程,多线程会导致复杂的同步问题
- 历史原因: 最初设计时为了简化浏览器脚本
事件循环机制
执行栈与任务队列
┌─────────────────────────────────────┐
│ 执行栈 (Call Stack) │
│ 执行同步代码和函数调用 │
└──────────────┬──────────────────────┘
│
▼ 当执行栈为空时
┌─────────────────────────────────────┐
│ 事件循环 (Event Loop) │
│ 不断检查执行栈和任务队列的状态 │
└──────────────┬──────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ 宏任务队列 │ │ 微任务队列 │
│ (MacroTask) │ │ (MicroTask) │
│ setTimeout │ │ Promise.then │
│ setInterval │ │ queueMicrotask │
│ I/O 操作 │ │ MutationObserver│
└──────────────┘ └──────────────────┘
执行顺序
- 执行同步代码
- 执行栈清空后,优先处理所有微任务
- 微任务队列清空后,取出一个宏任务执行
- 重复以上步骤
javascriptconsole.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4'); // 分析: // 1. console.log('1') -> 输出 1 // 2. setTimeout 是宏任务 -> 加入宏任务队列 // 3. Promise.then 是微任务 -> 加入微任务队列 // 4. console.log('4') -> 输出 4 // 5. 微任务队列:['3'] -> 输出 3 // 6. 宏任务队列:['2'] -> 输出 2 // 输出:1 -> 4 -> 3 -> 2
宏任务与微任务详解
宏任务(Macrotask)
| API | 说明 |
|---|---|
| setTimeout | 定时器 |
| setInterval | 间隔执行 |
| setImmediate | Node.js 立即执行 |
| I/O 操作 | 文件读写、网络请求 |
| UI 渲染 | 浏览器渲染(浏览器环境) |
| requestAnimationFrame | 动画帧 |
微任务(Microtask)
| API | 说明 |
|---|---|
| Promise.then/catch/finally | Promise 回调 |
| queueMicrotask() | 手动添加微任务 |
| MutationObserver | DOM 变化观察 |
| process.nextTick | Node.js 微任务 |
经典面试题分析
javascriptasync function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); async1(); new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); console.log('script end'); // 详细分析: // 1. console.log('script start') -> 输出 script start // 2. setTimeout 加入宏任务队列 // 3. async1() 调用 // - console.log('async1 start') -> 输出 async1 start // - await async2() 立即执行 async2() // - console.log('async2') -> 输出 async2 // - await 暂停 async1,将后续代码作为微任务加入队列 // 4. new Promise() 是同步的 // - console.log('promise1') -> 输出 promise1 // - resolve() 后,.then 加入微任务队列 // 5. console.log('script end') -> 输出 script end // 6. 微任务队列:['async1后续', 'promise2'] // - async1后续 -> 输出 async1 end // - promise2 -> 输出 promise2 // 7. 宏任务队列:['setTimeout'] // - setTimeout -> 输出 setTimeout // // 输出顺序: // script start // async1 start // async2 // promise1 // script end // async1 end // promise2 // setTimeout
Node.js 中的事件循环
Node.js 事件循环阶段
┌───────────────────────────┐
│ timers │ 执行 setTimeout 和 setInterval 回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ pending callbacks │ 执行延迟到下一个循环迭代的 I/O 回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ idle, prepare │ 内部使用
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ poll │ 检索新的 I/O 事件,执行 I/O 回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ check │ 执行 setImmediate 回调
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ close callbacks │ 执行关闭回调(如 socket.on('close'))
└───────────────────────────┘
setTimeout vs setImmediate
javascript// Node.js 环境中 setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); // 输出不确定,取决于系统性能 // 但如果放在 I/O 回调内,setImmediate 总是先执行
process.nextTick vs setImmediate
javascriptPromise.resolve() .then(() => console.log('promise.then')); process.nextTick(() => console.log('process.nextTick')); // 输出: // process.nextTick (nextTick 比微任务更高优先级) // promise.then
常见面试问题
Q1: 为什么 setTimeout(fn, 0) 不一定会立即执行?
即使延迟为 0,setTimeout 也会被加入宏任务队列。只有当执行栈和微任务队列都为空时,才会执行。
javascriptsetTimeout(() => console.log('timeout'), 0); Promise.resolve().then(() => { // 长时间运行的同步代码 let sum = 0; for (let i = 0; i < 1000000000; i++) { sum += i; } console.log('promise resolved'); }); console.log('sync code'); // 输出:sync code -> promise resolved -> timeout
Q2: async/await 和 Promise 微任务的关系?
async/await 是 Promise 的语法糖:
javascriptasync function example() { await promise; console.log('after await'); } // 相当于 function example() { return promise.then(() => { console.log('after await'); }); }
await 之后的代码会被包装成微任务。
Q3: 如何理解 JavaScript 的"非阻塞"?
javascript// 长时间计算不会阻塞主线程... 错! // JavaScript 是单线程,长时间同步计算会阻塞 UI // ❌ 错误:阻塞主线程 function heavyCalc() { let result = 0; for (let i = 0; i < 10000000000; i++) { result += i; } return result; } // ✅ 正确:使用 Web Worker(浏览器)或 Worker Threads(Node.js)
Q4: requestAnimationFrame vs setTimeout 动画?
| 特性 | requestAnimationFrame | setTimeout/setInterval |
|---|---|---|
| 帧率 | 与屏幕刷新率同步(60fps) | 需手动控制 |
| 页面隐藏 | 自动暂停 | 继续运行 |
| 节流 | 内置 | 需手动实现 |
| 性能 | 优化合并 | 可能丢帧 |
javascript// 使用 requestAnimationFrame 实现平滑动画 function animate(element) { let position = 0; function step() { position += 5; element.style.transform = `translateX(${position}px)`; if (position < 500) { requestAnimationFrame(step); } } requestAnimationFrame(step); }
最佳实践
- 避免长时间同步计算,使用 Web Worker
- 合理使用微任务,不要用微任务处理太复杂的逻辑
- 动画使用 requestAnimationFrame,不要用 setTimeout
- setTimeout(fn, 0) 不是立即执行,只是延迟到下一个宏任务
- Node.js 中注意 poll 阶段的 I/O 回调,可能阻塞事件循环