事件循环是前端异步代码绕不开的基础。只要页面里用了 Promise、async/await、定时器或接口请求,就会遇到执行顺序的问题。
先给一个实用结论:当前同步代码执行完后,JavaScript 会优先清空微任务队列,再进入下一轮任务去执行定时器等宏任务。setTimeout(fn, 0) 不是立刻执行,它只是把 fn 放进后续任务队列。
同步、微任务、宏任务分别是什么
同步代码会直接进入调用栈,按从上到下的顺序运行。
微任务常见来源是 Promise.then、catch、finally、queueMicrotask,以及 async/await 中 await 后面继续执行的部分。
宏任务常见来源是 setTimeout、setInterval、用户交互事件和部分 I/O 回调。不同运行环境对任务来源的细节有区别,但浏览器开发中先掌握这套顺序已经足够解决大多数问题。
看一个最常见的输出顺序
把下面代码复制到浏览器控制台:
| |
输出顺序是:
| |
原因是两条 console.log 都是同步代码,会先执行。Promise.then 进入微任务队列,setTimeout 回调进入后续任务队列。调用栈清空后,浏览器先执行微任务,最后才轮到定时器回调。
多个微任务按进入顺序执行
微任务不会互相抢占,而是按加入队列的先后顺序依次执行。
| |
输出顺序是 A -> B -> C -> D -> E。这里 Promise.then 比 queueMicrotask 先注册,所以它也先执行;两个都完成后,才会执行定时器回调。
async / await 为什么看起来像异步暂停
async 函数在遇到 await 前仍然是同步执行的。await 后面的代码会被放入微任务队列。
| |
输出是:
| |
所以 await 并不会阻塞整个页面,它只暂停当前 async 函数。其他同步代码仍然会先走完。
在 Vue 中什么时候用 nextTick
Vue 修改响应式数据后,不会每改一个值就立刻更新 DOM,而是会把更新合并。需要拿到更新后的真实 DOM 时,用 nextTick 比 setTimeout 更准确。
| |
nextTick 的语义是“等 Vue 完成本轮 DOM 更新后再执行”,而不是单纯延迟一会儿。它更适合处理弹窗打开后聚焦、列表更新后滚动到底部这类需求。
一个容易忽略的问题
微任务不是越多越好。每一轮宏任务结束后,浏览器会先清空当前微任务队列。若在微任务里不断创建新的微任务,渲染和用户交互就可能被推迟。
| |
这类代码会让浏览器一直处理微任务,页面可能失去响应。实际业务里,耗时计算应该拆分处理,或者交给 Web Worker,避免堵住主线程。
小结
理解事件循环后,排查异步顺序可以按这个步骤走:先找同步代码,再找 Promise 和 await 产生的微任务,最后看 setTimeout 等后续任务。大部分“为什么这个日志先出来”“为什么 DOM 还没更新”的问题,都会变得清楚很多。