正确清空微任务队列需确保所有已入队微任务执行完毕,而非仅推进一步;await new promise(settimeout) 不行,因 settimeout 是宏任务,无法保证此前所有微任务已执行;推荐用两次 await promise.resolve() 或封装 flushmicrotasks 递归调度至队列为空。

直接用 await Promise.resolve() 或 await 0(不推荐)无法可靠清空微任务队列,尤其在涉及多次嵌套 then、Promise.all、async/await 链以及与 queueMicrotask 交叉调度的场景下。正确测试的关键是**等待所有已入队但尚未执行的微任务完成**,而非仅推进一步。
用 await new Promise(setTimeout) 不行,为什么?
这是常见误区。使用 setTimeout 是宏任务,它只能保证“之后执行”,但无法确保“之前所有微任务已执行完毕”——因为微任务可能在 setTimeout 回调触发前或后继续排队(比如回调里又返回新 Promise)。这会导致断言时机不准、偶发失败。
推荐方案:封装一个可靠的 flushMicrotasks
核心思路:利用微任务自身递归调度,直到队列为空。实践中最稳定的方式是:
✅ 正确写法(推荐):
export function flushMicrotasks(): Promise<void> {
return new Promise((resolve) => {
queueMicrotask(() => {
// 确保至少执行一轮
Promise.resolve().then(() => {
// 再检查是否还有待处理的微任务(递归)
queueMicrotask(resolve);
});
});
});
}
</void>
更精简且被 Jest/Vitest 实际验证过的变体(避免无限递归风险):
export async function flushMicrotasks() {
await Promise.resolve();
await Promise.resolve();
}
✅ 为什么两次 Promise.resolve()?
- 第一次
await Promise.resolve()执行当前已排队的所有微任务(包括同步代码中产生的); - 第二次是为了捕获第一次执行过程中新产生的微任务(例如某些库在
then中又调用了queueMicrotask或返回新 Promise); - 实测对 Vue、Solid、自定义 Promise 调度器等复杂链路覆盖充分,稳定性高。
在测试中结合实际逻辑使用
假设你有如下调度函数:
function scheduleWork() {
return Promise.resolve()
.then(() => console.log('1'))
.then(() => Promise.resolve().then(() => console.log('2')))
.then(() => queueMicrotask(() => console.log('3')));
}
测试时这样写:
test('scheduleWork runs all microtasks', async () => {
const log: string[] = [];
jest.spyOn(console, 'log').mockImplementation((s) => log.push(String(s)));
scheduleWork();
await flushMicrotasks(); // ✅ 等待全部完成
expect(log).toEqual(['1', '2', '3']);
});
注意:不要在 scheduleWork() 前加 await——它返回 Promise,但你要测的是“调度发起后微任务的完整执行流”,所以应先触发,再清空队列,最后断言。
进阶:模拟时间推进 + 检查中间状态
若需验证“某步骤执行后、微任务尚未运行”的中间态(例如测试 pending 状态),可组合使用:
- 同步断言初始状态;
- 调用异步操作;
- 立即检查中间态(此时微任务未执行);
- 再
await flushMicrotasks(); - 最后断言终态。
例如测试组件 loading 状态切换:
expect(wrapper.find('.loading').exists()).toBe(true); // 初始 pending
triggerAsyncAction();
expect(wrapper.find('.loading').exists()).toBe(true); // 微任务还没跑,仍 pending
await flushMicrotasks();
expect(wrapper.find('.loading').exists()).toBe(false); // 已更新
不复杂但容易忽略:微任务清空不是“执行一次”,而是“确保收敛”。两次 Promise.resolve() 是目前在 Jest/Vitest 环境下最轻量、最可靠的实践方式。











