JavaScript是单线程语言,为了不阻塞主线程,运行时把异步回调拆成了宏任务与微任务两类队列。事件循环每推进一轮,都会先取出一个宏任务执行,随后把当前微任务队列一次性清空,再去取下一个宏任务。这种分层调度直接决定了我们写异步代码时的执行顺序。

一、宏任务与微任务分别是什么
宏任务(macrotask)是由宿主环境发起的任务,常见的包括整段script代码、setTimeout、setInterval、setImmediate(Node环境)、I/O操作、UI渲染等。每当事件循环进入新阶段,就会从宏任务队列中取一个来执行。微任务(microtask)则由JavaScript引擎自身发起,典型代表是Promise的then、catch、finally回调,以及MutationObserver和queueMicrotask。
两者的核心区别在于优先级:微任务总是在当前宏任务结束后、下一个宏任务开始前执行。也就是说,一个宏任务运行期间产生的微任务,不会等到下一轮循环,而是立刻被依次处理完。这种机制让Promise比setTimeout更早拿到结果,也避免了频繁让出主线程导致的延迟。
1.1 常见任务分类表
| 类型 | 具体示例 | 发起方 |
|---|---|---|
| 宏任务 | script、setTimeout、setInterval、I/O | 浏览器或Node宿主 |
| 微任务 | Promise.then、MutationObserver、queueMicrotask | JS引擎 |
上表可以帮助你快速判断一段异步代码会进入哪个队列。实际编码时,如果希望某个回调尽快执行且不要被渲染或其他宏任务插队,优先使用Promise或queueMicrotask。
二、从代码看执行顺序
下面这段代码展示了宏任务与微任务混用时真实的输出顺序。我们在script这个最外层宏任务中,先后注册了微任务和宏任务,观察它们如何被调度。
console.log('start'); // 同步代码,属于首个宏任务
setTimeout(function() {
console.log('timeout'); // 宏任务回调
}, 0);
Promise.resolve().then(function() {
console.log('promise'); // 微任务回调
});
console.log('end'); // 同步代码
运行后控制台依次打印:start、end、promise、timeout。原因是整段script作为第一个宏任务执行,同步部分直接输出start和end;接着引擎发现微任务队列里有Promise回调,于是清空微任务输出promise;最后事件循环取下一个宏任务,也就是setTimeout的回调,输出timeout。
如果把setTimeout换成嵌套结构,或者在一个微任务里再派发微任务,规则依然成立:微任务队列会被完全清空,才会轮到下一个宏任务。这也解释了为什么连续await多个Promise不会让定时器插进来。
2.1 微任务中再产生微任务
Promise.resolve().then(function() {
console.log('micro1');
Promise.resolve().then(function() {
console.log('micro2');
});
});
setTimeout(function() {
console.log('macro');
}, 0);
输出顺序是micro1、micro2、macro。可见在清空微任务时新入队的微任务也会在本轮被处理,而宏任务只能等待。这种“微任务优先且递归清空”的模型,是理解复杂异步流程的关键。
三、为什么需要这种分层设计
如果所有异步回调都放进同一个宏任务队列,那么像Promise这样需要高响应速度的API,就会被前面排队的I/O或定时器拖慢。微任务机制让引擎有机会在当前操作完成后立即续跑相关逻辑,不必等到浏览器渲染或下个时钟周期。
另一方面,若微任务没有限制地插队,又会导致宏任务饿死。因此规范限定每轮只取一个宏任务,但微任务要清空。这样既保证了交互流畅,也避免了定时器永远不执行。在Node和浏览器中具体实现略有差异,但调度原则一致。
3.1 实际开发中的注意点
- 不要在微任务里写无限循环或大量同步计算,否则会阻塞宏任务直至卡死页面。
- 需要精确控制节奏时,用setTimeout(fn, 0)把逻辑推到下一轮宏任务,避免和微任务纠缠。
- async函数中的await后面代码,本质也是微任务,不要误以为它和setTimeout同时段。
掌握宏任务与微任务的边界,不仅能预测输出顺序,还能在写动画、批量DOM更新、接口并发时做出更合理的异步安排。
四、快速自检方法
遇到拿不准的执行顺序,可以在脑中模拟事件循环:先跑同步代码,再从左到右扫一遍产生的微任务并清空,最后才看定时器之类的宏任务。多写几段对比代码,很快就能形成直觉。
记住:一个宏任务搭配一轮微任务清空,就是JavaScript异步调度的最小节拍。
当你能稳定说出任意混合代码片段的输出,就说明已经真正理解了宏任务与微任务在JavaScript运行时的角色与协作方式。
JavaScript宏任务微任务修改时间:2026-08-06 02:24:25