异步执行顺序由事件循环调度决定:微任务(Promise.then、queueMicrotask等)在当前宏任务结束后立即清空执行,宏任务(setTimeout等)每轮只取一个;DOM更新需配合nextTick或requestAnimationFrame确保时机。

遇到异步代码执行顺序不对、数据拿不到、DOM 更新不及时,别急着改逻辑——先看事件循环在怎么调度任务。
确认任务类型:宏任务还是微任务?
同一段代码里混用 setTimeout、Promise.then、queueMicrotask 时,执行顺序由队列优先级决定,不是书写顺序。微任务(如 Promise 回调、queueMicrotask)总在当前宏任务结束后立刻执行,且会清空整个微任务队列;宏任务(如 setTimeout、setInterval、I/O 回调、用户事件)则按进入队列的顺序,每轮事件循环只取一个。
- 打印日志时,把关键节点标记清楚:比如
console.log('sync') → setTimeout → Promise.then → console.log('end'),观察输出是否符合“同步 → 微任务 → 宏任务”节奏 - 用
queueMicrotask(() => console.log('micro'))替代Promise.resolve().then(...)做最小验证,避免 Promise 构造开销干扰判断 - 注意
MutationObserver回调也属于微任务,和 Promise 同级,但常被忽略
检查 await 是否真正生效
await 只对 Promise 或 thenable 起作用,且必须在 async 函数内。如果忘记 await,协程对象会被静默创建却不执行,后续代码不会等待。
- 写完
fetch或自定义异步函数后,立刻检查返回值类型:typeof fn() === 'object' && fn().then成立,大概率是忘了 await - 不要链式写
fetch(...).then(...).then(...)混合 async/await,容易漏掉某一层的 await;统一用const res = await fetch(...); const data = await res.json() - 在调试器中打断点,看执行流是否真的停在 await 行;若直接跳过,说明该表达式未返回 Promise 或未被 await
留意 DOM 更新时机与 nextTick 类机制
Vue 的 nextTick、React 的 useEffect 或原生 requestAnimationFrame 都依赖事件循环阶段。DOM 批量更新通常发生在微任务之后、渲染之前,而某些操作(如获取 offsetWidth)需要等布局完成。
- 修改 state 后立即读取 DOM 尺寸?大概率拿到旧值。应放在
queueMicrotask或requestAnimationFrame中,前者确保 DOM 已更新但未渲染,后者确保渲染完成 - 多个
setState或ref.value = ...连续触发时,框架会合并更新,但回调时机仍受微任务队列影响;可加await nextTick()显式等待 - 监听
input事件时,e.target.value是实时的,但绑定的 reactive 数据可能滞后一帧——这不是 bug,是事件循环与响应式系统协同的结果
排查阻塞与长任务干扰
事件循环本身不卡,但同步代码跑太久,会延迟所有异步回调的执行。哪怕写了 setTimeout(fn, 0),只要主线程被大循环或复杂计算占满,它就得排队等到空闲。
- 用浏览器 DevTools 的 Performance 面板录制,看是否有超过 50ms 的长任务(Long Task),这是导致异步回调明显延迟的常见原因
- 把密集计算拆成小块,用
setTimeout或queueMicrotask分片执行,给事件循环留出响应间隙 - 注意
while(Date.now() - start 这类忙等待会彻底锁死主线程,必须改用定时器或 requestIdleCallback

















