标准答案
- 执行模型:JavaScript 主线程一次只能执行一个调用栈。同步代码先执行,异步操作完成后把回调放进任务队列,事件循环负责把队列里的任务拿回主线程执行。
- 宏任务和微任务:一次事件循环通常会先执行一个宏任务,宏任务结束后清空本轮产生的微任务队列,然后浏览器才有机会渲染,再进入下一个宏任务。常见宏任务有 script、setTimeout、setInterval、用户交互事件;常见微任务有 Promise then/catch/finally、queueMicrotask、MutationObserver。
- 判断顺序:先执行同步代码;遇到 Promise 构造函数时里面的代码立即执行;then/catch/finally 放进微任务;setTimeout 这类回调进入后续宏任务。所以通常是同步代码、微任务、下一个宏任务。
- 工程影响:微任务会在当前宏任务结束后尽快清空,如果递归产生大量微任务,可能延迟渲染和用户交互。前端性能问题里要注意长任务和微任务堆积。
题目解析
事件循环不能只记“宏任务有哪些、微任务有哪些”的清单,更重要的是理解一次循环里的顺序:一个宏任务执行完后,会先清空微任务队列,然后浏览器才有机会渲染,再进入下一个宏任务。
Promise 构造函数立即执行,这是很多输出题的陷阱。异步的是 then/catch/finally 回调,不是 new Promise 里的同步代码。解析输出题时,先标同步日志,再标微任务,再标后续宏任务,基本不会乱。
浏览器渲染不是每行 JS 后都发生。长同步任务会阻塞渲染,过多微任务也可能延迟渲染,所以事件循环不仅影响输出顺序,也和页面卡顿、首屏响应有关。
Node.js 也有事件循环,但阶段模型和浏览器不同。前端场景优先按浏览器模型理解,再补充 Node.js 的差异边界。
代码示例
下面这段代码可以用来拆解同步任务、微任务和宏任务的执行顺序。
JavaScript
console.log('sync 1')
setTimeout(() => {
console.log('timeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise then')
})
new Promise(resolve => {
console.log('promise constructor')
resolve()
})
console.log('sync 2')
// sync 1
// promise constructor
// sync 2
// promise then
// timeout常见误区
- 误以为 Promise 构造函数里的代码是异步的。构造函数立即同步执行。
- 误以为每执行一个微任务就会渲染一次。通常会先清空微任务队列。
- 忽略微任务递归产生微任务可能阻塞渲染。