JavaScript事件循环机制与宏任务微任务执行顺序
JavaScript 是单线程语言,但能处理异步操作(如网络请求、定时器),靠的就是事件循环机制。理解事件循环的关键,在于分清宏任务(macrotask)和微任务(microtask)的执行顺序。
识别任务类型
区分哪些代码属于宏任务,哪些属于微任务:
-
宏任务包括:
setTimeout、setInterval- I/O 操作(如文件读取、网络请求)
- UI 渲染
script标签整体执行(即整个脚本的首次运行)
-
微任务包括:
Promise.then/Promise.catch/Promise.finallyqueueMicrotaskMutationObserver(用于监听 DOM 变化)
注意:
async/await本质是基于 Promise 的语法糖,因此await后的代码会进入微任务队列。
执行流程规则
JavaScript 引擎按以下步骤运行代码:
- 执行当前宏任务(例如整个
<script>脚本或一个setTimeout回调)。 - 收集该宏任务中产生的所有微任务,放入微任务队列。
- 立即执行微任务队列中的全部任务,直到队列清空。
- 执行一次 UI 渲染(如果需要)。
- 从宏任务队列中取出下一个宏任务,重复上述过程。
关键点:微任务总是在当前宏任务结束后、下一个宏任务开始前全部执行完。
实战验证执行顺序
观察以下代码的输出顺序:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
执行步骤分解:
- 整个脚本作为第一个宏任务开始执行。
- 打印
'1'。 - 遇到
setTimeout,将其回调(打印'2')加入宏任务队列。 - 遇到
Promise.resolve().then,将其回调(打印'3')加入微任务队列。 - 打印
'4'。 - 当前宏任务结束,立即执行所有微任务 → 打印
'3'。 - 微任务队列清空后,浏览器可能进行渲染。
- 从宏任务队列取出
setTimeout回调 → 打印'2'。
最终输出顺序为:
1
4
3
2
嵌套场景下的执行顺序
当微任务中又产生新的微任务或宏任务时,规则依然适用:
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise in timeout1');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => {
console.log('timeout in promise1');
}, 0);
Promise.resolve().then(() => {
console.log('promise2');
});
});
console.log('end');
逐步分析:
-
主宏任务执行:
- 打印
'start' setTimeout回调(含'timeout1')入宏任务队列- 第一个
Promise.then(含'promise1')入微任务队列 - 打印
'end'
- 打印
-
主宏任务结束,执行微任务队列:
- 打印
'promise1' - 其中的
setTimeout(含'timeout in promise1')入宏任务队列 - 其中的第二个
Promise.then(含'promise2')入微任务队列 - 继续执行新加入的微任务 → 打印
'promise2' - 微任务队列清空
- 打印
-
开始处理宏任务队列(先进先出):
- 执行第一个
setTimeout→ 打印'timeout1' - 其中的
Promise.then(含'promise in timeout1')入微任务队列 - 当前宏任务结束,立即执行该微任务 → 打印
'promise in timeout1'
- 执行第一个
-
继续下一个宏任务:
- 执行
'timeout in promise1'→ 打印'timeout in promise1' - 无新微任务,结束
- 执行
最终输出:
start
end
promise1
promise2
timeout1
promise in timeout1
timeout in promise1
常见误区澄清
| 误解 | 正确理解 |
|---|---|
“setTimeout(fn, 0) 会立刻执行” |
实际是将 fn 放入宏任务队列,需等当前宏任务及所有微任务执行完才轮到它 |
| “微任务只在 Promise 中出现” | queueMicrotask 也能手动添加微任务 |
“多个 setTimeout 会按时间精确排序” |
即使设为 0ms,也受浏览器最小延迟限制(通常 ≥4ms),且执行顺序依赖入队顺序 |
实用调试技巧
使用 queueMicrotask 明确控制微任务:
console.log('A');
queueMicrotask(() => console.log('B'));
setTimeout(() => console.log('C'), 0);
console.log('D');
输出:
A
D
B
C
这验证了:queueMicrotask 的行为与 Promise.then 一致,都属于微任务。
总结执行优先级
任务执行的绝对优先级顺序为:
- 当前宏任务中的同步代码
- 所有已入队的微任务(按入队顺序,深度优先)
- 下一个宏任务(按入队顺序)
记住这个口诀:
“宏任务一跑完,微任务全清空;清完再跑下一宏”。

暂无评论,快来抢沙发吧!