标准答案

  1. 执行模型:JavaScript 主线程一次只能执行一个调用栈。同步代码先执行,异步操作完成后把回调放进任务队列,事件循环负责把队列里的任务拿回主线程执行。
  2. 宏任务和微任务:一次事件循环通常会先执行一个宏任务,宏任务结束后清空本轮产生的微任务队列,然后浏览器才有机会渲染,再进入下一个宏任务。常见宏任务有 script、setTimeout、setInterval、用户交互事件;常见微任务有 Promise then/catch/finally、queueMicrotask、MutationObserver。
  3. 判断顺序:先执行同步代码;遇到 Promise 构造函数时里面的代码立即执行;then/catch/finally 放进微任务;setTimeout 这类回调进入后续宏任务。所以通常是同步代码、微任务、下一个宏任务。
  4. 工程影响:微任务会在当前宏任务结束后尽快清空,如果递归产生大量微任务,可能延迟渲染和用户交互。前端性能问题里要注意长任务和微任务堆积。

题目解析

事件循环不能只记“宏任务有哪些、微任务有哪些”的清单,更重要的是理解一次循环里的顺序:一个宏任务执行完后,会先清空微任务队列,然后浏览器才有机会渲染,再进入下一个宏任务。

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 构造函数里的代码是异步的。构造函数立即同步执行。
  • 误以为每执行一个微任务就会渲染一次。通常会先清空微任务队列。
  • 忽略微任务递归产生微任务可能阻塞渲染。

作者信息