JavaScript任务队列包含宏任务和微任务两套机制:宏任务如setTimeout、脚本执行等每次只执行一个,中间插入UI渲染;微任务如Promise.then等在每个宏任务结束后立即清空执行,优先级更高且不触发渲染。

JavaScript 任务队列不是“一个队列”,而是两套并行协作的机制:宏任务队列和微任务队列。真正决定代码执行顺序的,是它们之间的优先级规则和事件循环的调度节奏。
宏任务与微任务,分工明确
宏任务(Macro Task)负责大块、可观测的执行单元,比如整体脚本、setTimeout、setInterval、I/O 操作、UI 渲染等。它们每次只执行一个,执行完就让出控制权。
微任务(Micro Task)则更轻量、更高优先级,包括 Promise.then/catch/finally、MutationObserver、queueMicrotask 等。它的特点是:只要当前宏任务结束,JS 引擎会立刻清空整个微任务队列,一个不落地连续执行。
- 宏任务之间必须“轮着来”,中间插入 UI 渲染等操作
- 微任务不排队等下一轮,而是在每个宏任务结束后“插队执行”
- 哪怕在微任务里再创建新的微任务,也会被加入当前轮次的队尾,继续执行直到清空
事件循环怎么跑起来的?
事件循环不是一次性流程,而是一个持续运转的闭环:
立即学习“Java免费学习笔记(深入)”;
- 先执行同步代码(属于第一个宏任务)
- 同步代码结束,立即检查并执行所有待处理的微任务
- 微任务队列清空后,从宏任务队列中取出下一个(最早加入的那个)执行
- 重复上述步骤:宏任务 → 微任务 → 宏任务 → ……
注意:浏览器会在两个宏任务之间进行一次 UI 渲染(如重绘页面),但微任务不会触发渲染——这也是为什么用 Promise.then 更新 DOM 后,往往要等下一帧才能看到效果。
常见误区:setTimeout(0) 不等于“立刻执行”
setTimeout(fn, 0) 只表示“尽可能快地安排”,但它仍属于宏任务。它的回调必须等到当前宏任务+全部微任务完成,并且至少经过一次事件循环周期后,才会进入执行队列。
对比之下:queueMicrotask(fn) 或 Promise.resolve().then(fn) 才是真正“下一拍就执行”的方式——只要当前同步代码一结束,它就会抢占执行权。
- 想让逻辑紧接当前操作之后运行 → 选微任务
- 想让逻辑延后到下一轮事件循环开始 → 选 setTimeout(0)
- 想确保 DOM 更新已生效再操作 → 通常需结合 requestAnimationFrame 或 next tick
实际调试小技巧
遇到执行顺序异常时,别急着猜,用 console.log + 标记法快速定位:
- 在关键位置打印带前缀的日志,比如 “[sync]”、“[macro]”、“[micro]”
- 把 Promise.then 写成
.then(() => console.log('[micro]')),把 setTimeout 写成setTimeout(() => console.log('[macro]'), 0) - 观察输出顺序,就能验证你对任务类型的判断是否准确
任务队列机制本身不复杂,但容易因异步嵌套而模糊边界。抓住“宏任务轮转 + 微任务清空”这个核心节拍,多数顺序问题就迎刃而解。


















