微任务队列在宏任务结束后一次性清空:从队首取出执行,新微任务追加队尾,持续至队列为空;引擎设递归限制防卡死,宏任务可强制中断长链。

微任务队列会在当前宏任务执行完后,**一次性清空所有已存在的微任务**,但这个“清空”不是指禁止新增,而是指:只要队列非空,就持续取出并执行首个微任务;而新创建的微任务(比如在 Promise 回调里再调用 Promise.resolve().then(...))会**追加到当前微任务队列末尾**,继续被本轮清空过程消费——直到队列彻底为空才停止。
微任务队列是“单轮次、深度优先”的执行模型
浏览器或 Node.js 的事件循环在每次宏任务(如 script 执行、setTimeout 回调)结束后,会进入微任务检查点。此时它不会只执行一个微任务就跳出,而是:
- 从微任务队列头部取出第一个任务执行
- 执行过程中若产生新的微任务(如
then、queueMicrotask),它们被添加到队列尾部 - 只要队列不为空,就重复取头、执行、追加的过程
- 直到队列完全为空,才退出微任务阶段,继续下一个宏任务
这本质上是一个“执行-生成-再执行”的循环,终止条件**唯一且明确:队列长度变为 0**。
递归创建微任务不会无限卡死,但可能引发实际问题
代码可以写成这样:
用于 inference.sh 的 JavaScript/TypeScript SDK,可运行 AI 应用、构建代理、集成 150+ 模型。包名:@inferencesh/sdk(npm install),完整 TypeScript 支持。
queueMicrotask(() => {
console.log('tick');
queueMicrotask(arguments.callee); // 旧式递归(不推荐)
});
// 或更常见的:
Promise.resolve().then(() => {
console.log('tick');
Promise.resolve().then(arguments.callee);
});
这类代码理论上会一直往队列尾部加任务,导致微任务队列永远不为空——但现实中,JavaScript 引擎有保护机制:
- V8(Chrome/Node)对连续微任务执行设有限制(如约 1000 层嵌套后抛出 RangeError)
- Firefox 和 Safari 也有类似栈深或执行耗时检测
- 这不是规范要求,而是实现层面的防崩策略
真正决定循环结束的是“无新任务入队”而非“无递归”
只要微任务执行逻辑中不再调用 then、catch、queueMicrotask、MutationObserver 等触发微任务的 API,队列自然会在当前轮次内清空。例如:
- 一个
Promise.then(() => { console.log(1); })不产生新微任务 → 执行完队列即空 - 一个
Promise.then(() => Promise.resolve().then(() => {}))会产生一个新微任务 → 队列需再执行一次才空 - 如果某个微任务内部做了条件判断,只在特定情况下再发微任务,那循环就由业务逻辑控制退出时机
注意:宏任务是天然的“断点”,可打破长链
如果你希望避免微任务无限累积,主动引入宏任务是个可靠方式:
- 用
setTimeout(fn, 0)或postMessage把后续逻辑推到下一轮宏任务 - 这样当前微任务队列必能清空,执行权交还给事件循环主流程
- 适合做节流、防阻塞、或需要 DOM 渲染机会的场景(因为微任务后紧接渲染,宏任务后才有渲染机会)

















