事件循环(Event Loop)—— 深度解析

一、先看概念(最重要的要点)

  1. Call Stack(调用栈):执行同步代码的地方。JS 引擎在这里逐帧执行函数调用。
  2. 任务(Task / macrotask)队列:例如 setTimeoutsetInterval、用户交互事件(click)、UI 事件、postMessage 等。每次从任务队列取一个任务执行(形成一个 macrotask)。
  3. 微任务(Microtask / job)队列:例如 Promise.then / catchqueueMicrotaskMutationObserver 的回调(都是微任务)。微任务在当前 macrotask 执行结束后立刻运行,且会循环执行直到队列清空,再进入下一个 macrotask。
  4. 渲染(layout / paint):浏览器会在合适的时机在 macrotask / microtask 处理完后触发渲染。渲染前后还有 requestAnimationFrame 的回调点(rAF 在下一帧渲染前运行)。
  5. 关键结论:同步代码 ->(macrotask 中) -> 所有微任务(直到空) -> 渲染/rAF(若需要) -> 下一个 macrotask。

二、任务类型和常见来源(举例)

  • 宏任务(macrotask)示例

    • 初始脚本(页面加载的 script)
    • setTimeout / setInterval
    • UI 事件(click、keydown)
    • I/O 回调、postMessage(某些实现)
  • 微任务(microtask)示例

    • Promise.then / Promise.catch / Promise.finally
    • queueMicrotask(...)
    • MutationObserver 回调
  • 渲染相关

    • requestAnimationFrame(rAF):在下一帧“绘制前”执行(在微任务之后、绘制之前)
  • Node 环境的特别项

    • process.nextTick():Node 的专用队列,优先级高于 Promise microtasks(能在当前 tick 的末尾立刻执行,注意可导致 starvation)。
    • setImmediate():libuv 的 check phase,行为与 setTimeout(...,0) 在某些情形不同(和 I/O 有关)。

三、执行顺序(浏览器版)—— 一个简化时间线

[ 开始一个 macrotask(比如执行一个 script) ]
  ↓ 执行同步代码(Call Stack)
  ↓ macrotask 结束 → 执行微任务队列(microtasks)直到为空(Promise.then 等)
  ↓ 若需要,执行 rAF 回调(在渲染前)
  ↓ 浏览器执行 layout / paint(渲染)
  ↓ 进入下一个 macrotask(比如下一个 setTimeout 回调或事件处理)

注意:微任务是“会把控制权留在当前线程直到队列清空”的 —— 如果你不停新增微任务,会导致渲染和下一个 macrotask 被延迟(可能造成卡顿)。


四、典型示例与精确输出(请按顺序理解原因)

示例 A — setTimeout vs Promise.then

console.log('start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve().then(() => console.log('promise'));

console.log('end');

输出顺序

start
end
promise
setTimeout

原因setTimeout 的回调是 macrotask,Promise.then 的回调是 microtask;当前 macrotask(script)结束后先跑微任务,再跑下一个 macrotask(setTimeout)。


示例 B — requestAnimationFrame、微任务 与 setTimeout

console.log('start');

requestAnimationFrame(() => console.log('rAF'));

Promise.resolve().then(() => console.log('promise'));

setTimeout(() => console.log('timeout'), 0);

console.log('end');

常见输出(浏览器):

start
end
promise
rAF
timeout

原因:微任务在当前 macrotask 后立即执行;rAF 回调在下一个可用帧的“绘制前”执行(也就是在微任务之后、在渲染和绘制之前);setTimeout 在后面的 macrotask 中执行。


示例 C — async/awaitPromise.then 的相对顺序

console.log('script start');

async function foo() {
  console.log('foo start');
  await null;           // 等同于:Promise.resolve(null).then(...)
  console.log('foo end');
}

foo();

Promise.resolve().then(() => console.log('promise callback'));

console.log('script end');

输出顺序(浏览器):

script start
foo start
script end
foo end
promise callback

原因

  • await 会把后续代码拆成微任务(await 遇到时立即把“后续”排入 microtask)。
  • foo() 中遇到 await 时就把 foo 的“后续”微任务排入队列(这个排入发生在执行到 await 的那一刻)。
  • 之后全局的 Promise.resolve().then(...) 再被排入微任务队列(排在 foo 的后面)。microtasks 是 FIFO,所以 foo endpromise callback 之前。

面试点await 不是把函数继续执行放入宏任务,而是微任务(和 .then() 一样),只是语法更清晰。


示例 D — Node 环境:process.nextTick vs Promise.then

在 Node 中:

console.log('start');

process.nextTick(() => console.log('nextTick'));

Promise.resolve().then(() => console.log('promise'));

console.log('end');

Node 输出顺序

start
end
nextTick
promise

原因process.nextTick 属于 Node 的专用队列,它在本轮事件循环“结束前”优先于 Promise microtasks 执行(注意:process.nextTick 太多会阻塞后续 I/O)。


五、细节与面试常考点(要点总结)

  1. 微任务会“饿死”渲染/宏任务:如果微任务在执行过程中不断产生新的微任务,浏览器会一直清空微任务队列而延迟绘制和后续 macrotask,导致界面卡顿。
  2. await 等价于把后续逻辑排到 microtask,因此 await 后面的代码会在当前 macrotask 结束后、微任务阶段运行。
  3. setTimeout(fn, 0) 并非“立即”执行:它只是把回调加入任务队列,等待下一个 macrotask。浏览器还可能对连续短定时器进行最小间隔限制(clamp),且后台标签页会被 throttle。
  4. requestAnimationFrame 在下一帧的“绘制前”执行(可用于做动画前的 DOM 读取/写入)。rAF 与微任务的顺序:微任务先执行 -> rAF 回调(若存在) -> paint。
  5. Node 的事件循环比浏览器更复杂(libuv 多阶段),且 Node 有 process.nextTicksetImmediate 两个特殊 API(优先级和阶段不同)。
  6. MutationObserver 回调是微任务队列的一部分(常用于“异步 DOM 变更通知”)。

六、常见陷阱 & 优化建议

  • 不要在微任务中放过重计算或循环创建微任务,会导致 UI 无法及时刷新。

  • 如果需要让浏览器完成渲染再执行某个操作:

    • 可以用 requestAnimationFrame(在渲染前)配合 setTimeout(...,0) 做“渲染后”的粗略保证;或使用两次 rAF:rAF(() => rAF(() => { /* 已完成一帧绘制 */ }))(常见技巧,但细节依实现而异)。
  • 调试顺序:用 console.log 标记顺序;Chrome DevTools 的 Performance 面板可以捕获帧、回调与主线程卡顿。

  • 在 Node 中避免滥用 process.nextTick(会阻塞 I/O);Promise microtasks 更“规范”。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐