JavaScript 事件循环与宏任务/微任务
JavaScript 是单线程语言,通过事件循环实现异步非阻塞,理解宏任务和微任务的执行顺序至关重要。
事件循环模型
JavaScript
┌─────────────────────────────────────────┐
│ 事件循环流程 │
├─────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ 调用栈 │ ←→ │ 宿主环境 │ │
│ │ (Call │ │ (浏览器/Node.js) │ │
│ │ Stack) │ └──────────────────┘ │
│ └────┬─────┘ ↓ │
│ │ ┌──────────────┐ │
│ │ │ 宏任务队列 │ │
│ │ │ (Task Queue) │ │
│ │ └──────────────┘ │
│ ↓ ↓ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ 微任务队列 │ │ 执行一个宏任务 │ │
│ │(Microtask │ │ │ │
│ │ Queue) │ │ │ │
│ └──────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────┘
执行顺序
JavaScript
1. 执行同步代码(调用栈)
2. 调用栈清空后,执行所有微任务
3. 执行一个宏任务
4. 重复步骤 2-3
宏任务(Macro Task)
常见宏任务
| 来源 | API |
|---|---|
| 定时器 | setTimeout, setInterval |
| I/O | 文件读写、网络请求 |
| UI 渲染 | requestAnimationFrame |
| 事件 | 用户交互事件 |
| setImmediate | Node.js |
宏任务示例
JavaScript
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
console.log('script end');
// 输出:
// script start
// script end
// setTimeout
微任务(Micro Task)
常见微任务
| 来源 | API |
|---|---|
| Promise | then, catch, finally |
| queueMicrotask | queueMicrotask() |
| MutationObserver | DOM 变化监听 |
| process.nextTick | Node.js(优先级最高) |
微任务示例
JavaScript
console.log('script start');
Promise.resolve()
.then(() => console.log('promise1'))
.then(() => console.log('promise2'));
console.log('script end');
// 输出:
// script start
// script end
// promise1
// promise2
完整执行顺序
JavaScript
console.log('1. script start');
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
Promise.resolve()
.then(() => console.log('3. promise1'))
.then(() => console.log('4. promise2'));
console.log('5. script end');
// 执行过程:
// 1. 同步执行:1, 5
// 2. 调用栈清空,执行微任务:3, 4
// 3. 执行宏任务:2
// 输出:
// 1. script start
// 5. script end
// 3. promise1
// 4. promise2
// 2. setTimeout
复杂案例分析
嵌套微任务
JavaScript
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
.then(() => {
console.log('3');
Promise.resolve()
.then(() => console.log('4'));
})
.then(() => console.log('5'));
console.log('6');
// 输出:1, 6, 3, 4, 5, 2
async/await 执行
JavaScript
async 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');
// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
async/await 等价转换
JavaScript
async function foo() {
console.log(1);
await bar();
console.log(3);
}
function bar() {
console.log(2);
}
// 等价于
function foo() {
console.log(1);
return Promise.resolve(bar()).then(() => {
console.log(3);
});
}
requestAnimationFrame
JavaScript
console.log('script start');
requestAnimationFrame(() => {
console.log('raf');
});
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('setTimeout'), 0);
console.log('script end');
// 输出(浏览器):
// script start
// script end
// promise
// raf(渲染前执行)
// setTimeout
raf 与事件循环
JavaScript
宏任务 → 微任务 → requestAnimationFrame → 渲染 → 宏任务
Node.js 差异
process.nextTick
JavaScript
// Node.js 特有,优先于 Promise
process.nextTick(() => {
console.log('nextTick');
});
Promise.resolve().then(() => {
console.log('promise');
});
// 输出:nextTick, promise
Node.js 事件循环阶段
text
┌───────────────────────┐
┌─>│ timers │ setTimeout/setInterval
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ pending callbacks │ I/O 回调
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ idle, prepare │
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ poll │ 轮询
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
│ │ check │ setImmediate
│ └───────────┬───────────┘
│ ┌───────────┴───────────┐
└──┤ close callbacks │
└───────────────────────┘
setTimeout vs setImmediate
text
// 在 I/O 回调中
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 输出:immediate, timeout(setImmediate 先执行)
// 在主模块中
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出不确定,取决于 CPU 性能
实践技巧
优先使用微任务
text
// ✅ 使用微任务进行异步操作
Promise.resolve().then(doSomething);
// ❌ 避免不必要的宏任务
setTimeout(doSomething, 0);
批量更新
text
let pending = false;
function queueUpdate() {
if (!pending) {
pending = true;
Promise.resolve().then(() => {
pending = false;
// 批量处理更新
flushUpdates();
});
}
}
微任务在每次事件循环之间执行,宏任务在事件循环中执行。合理利用可优化性能。
要点总结
| 类型 | 示例 | 执行时机 |
|---|---|---|
| 同步代码 | 主线程代码 | 最先 |
| 微任务 | Promise.then | 同步后,宏任务前 |
| 宏任务 | setTimeout | 每次循环一个 |
- 同步代码优先执行
- 微任务队列清空后才执行宏任务
- 宏任务每次只执行一个
- Node.js 中 nextTick 优先于 Promise
- async/await 是 Promise 的语法糖
📝 发现内容有误?点击此处直接编辑