宏任务和微任务是事件循环中真实调度的两个队列:宏任务由宿主环境推入(如setTimeout、DOM事件、I/O),微任务由JS引擎内部触发(如Promise.then、queueMicrotask);执行顺序为“执行一个宏任务→立即清空全部微任务→再取下一个宏任务”。

宏任务和微任务不是语法关键词,而是事件循环中真实调度的两个队列。写错顺序不报错,但状态更新延迟、UI渲染异常、接口竞态、动画卡顿这些“玄学问题”,十有八九就出在这里。
一眼识别:哪些是宏任务,哪些是微任务
不用死记,记住两条主线:
-
宏任务:由浏览器或 Node.js 环境“主动推入”的异步回调,通常涉及外部系统介入——定时器(
setTimeout/setInterval)、I/O(fetch回调、XMLHttpRequest)、DOM 事件(click、input)、UI 渲染、requestAnimationFrame、MessageChannel、Node.js 的setImmediate -
微任务:由 JS 引擎“内部触发”的响应型操作,特点是轻量、高优先、链式强——
Promise.then/catch/finally、queueMicrotask、MutationObserver回调;Node.js 中process.nextTick优先级更高,但仍是微任务范畴
关键提醒:async 函数本身是同步执行的(进入调用栈),await 后面的代码会被包装成微任务;new Promise 构造函数内的代码是同步执行的,只有 resolve() 被调用后,其 .then 才进微任务队列。
执行顺序铁律:清空,不是执行一个
事件循环不是“先微后宏”,而是:
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
- 执行完当前宏任务(含所有同步代码)
-
立刻、一次性清空当前全部微任务(哪怕中间又
Promise.resolve().then新增了微任务,也会立即追加并执行) - 然后才取下一个宏任务
这个“清空”机制解释了为什么连续三个 .then 会连着输出,而 setTimeout 总是被挤到最后;也说明为什么在微任务里反复 queueMicrotask 可能导致饥饿(主线程一直没机会处理宏任务),要小心递归滥用。
高频踩坑场景与写法建议
以下不是理论,是线上真出过问题的模式:
-
状态更新后 DOM 没刷新? —— 你在微任务里改了
state,但没等浏览器渲染就直接读offsetHeight。解决:把读取逻辑放到setTimeout(下一轮宏任务)或requestAnimationFrame(紧邻渲染前) -
多个
fetch并发,最后只拿到最后一个结果? —— 每个fetch的.then是微任务,但你用同一个变量赋值,后到的覆盖前到的。解决:用Promise.all或给每个请求绑定独立作用域,避免竞态 -
组件卸载后还执行
setState报错? ——fetch.then(setState)是微任务,请求返回时组件可能已销毁。解决:在微任务执行前加校验(如 React 的useEffect清理函数标记isMounted),或改用AbortController主动中断 -
想让某段逻辑“等本轮所有异步结束再跑”,却用了
setTimeout(fn, 0)? —— 它只是进了宏任务队列,前面可能还有别的宏任务(比如用户点击、滚动事件)。更稳写法:queueMicrotask(fn),它会在当前宏任务结束后、任何新宏任务开始前执行
调试技巧:不靠猜,靠看
别只盯着代码顺序,打开浏览器 DevTools:
- 在关键位置加
console.log,配合%c样式区分:微任务打上color: blue,宏任务用color: red - Chrome 的
Performance面板录制后,展开Event Log,能看到每个Timer Fired(宏任务)、PromiseResolve(微任务)的时间戳和嵌套关系 - Node.js 环境可用
--trace-event-categories v8,devtools.timeline输出事件日志,定位任务排队位置
真正理解它们,不是为了画事件循环图,而是让每一行异步代码都落在你预期的位置上。

















