JavaScript 里常说的两个队列:
#JavaScript 里常说的两个队列:
#1. 宏任务队列(Macrotask Queue)
存放一轮一轮执行的“大任务”,常见:
setTimeoutsetIntervalsetImmediate(Node.js)- I/O 回调
- UI 事件回调
- 部分宿主环境任务
例如:
setTimeout(() => {
console.log("宏任务");
}, 0);
#2. 微任务队列(Microtask Queue)
存放当前任务执行完后,立即清空的“小任务”,常见:
Promise.then / catch / finallyqueueMicrotaskMutationObserverprocess.nextTick(Node.js,优先级还有特殊规则)
例如:
Promise.resolve().then(() => {
console.log("微任务");
});
#执行顺序
可以记成:
宏任务
↓
执行同步代码
↓
清空微任务队列
↓
浏览器进行渲染等
↓
下一个宏任务
↓
清空微任务队列
↓
...
经典例子:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
输出:
1
4
3
2
核心记忆:
同步代码 → 微任务全部执行 → 下一个宏任务
所以 Promise.then() 通常会比 setTimeout(..., 0) 更早执行。
可以这样理解:每执行完一个宏任务,就会检查并清空一次微任务队列。
更准确的完整循环是:
┌─────────────────────┐
│ 一个宏任务 │
│ │
│ 执行同步代码 │
└──────────┬──────────┘
↓
清空微任务队列
↓
浏览器可能进行渲染
↓
┌─────────────────────┐
│ 下一个宏任务 │
└──────────┬──────────┘
↓
清空微任务队列
↓
浏览器可能进行渲染
↓
┌─────────────────────┐
│ 再下一个宏任务 │
└──────────┬──────────┘
↓
清空微任务队列
↓
...
#举个特别直观的例子
console.log("同步1");
setTimeout(() => {
console.log("宏任务1");
Promise.resolve().then(() => {
console.log("微任务1");
});
}, 0);
setTimeout(() => {
console.log("宏任务2");
Promise.resolve().then(() => {
console.log("微任务2");
});
}, 0);
执行过程:
第一个宏任务:script
↓
同步1
↓
清空微任务
↓
第二个宏任务:setTimeout 1
↓
宏任务1
↓
发现微任务1
↓
立即执行微任务1
↓
第三个宏任务:setTimeout 2
↓
宏任务2
↓
发现微任务2
↓
立即执行微任务2
所以输出:
同步1
宏任务1
微任务1
宏任务2
微任务2
#最重要的一句话
一个宏任务
↓
把这个宏任务产生的所有微任务清空
↓
下一个宏任务
↓
把这个宏任务产生的所有微任务清空
↓
下一个宏任务
而且清空微任务时,如果微任务又产生新的微任务,会继续执行,直到微任务队列彻底为空。
例如:
Promise.resolve().then(() => {
console.log("微任务1");
Promise.resolve().then(() => {
console.log("微任务2");
});
});
结果:
微任务1
微任务2
这就是为什么可以把它记成:
宏任务之间穿插微任务;每个宏任务结束后,微任务队列清空。