JavaScript 事件循环 (Event Loop) 全队列单步推演沙盒(光杆站长模型)

配套《一文讲透事件循环》博文的交互式沙盒:还原“光杆站长”心智模型,逐帧单步推演调用栈、Promise 微任务、requestAnimationFrame、setTimeout 宏任务与 requestIdleCallback 执行时序。

🚉 光杆站长当前状态:准备就绪
Step 1 / 1
💡

正在加载 Rust Wasm 事件循环引擎...

📜 JavaScript 代码执行跟踪Line #1
1function slowAction() {
2 console.log('执行栈:慢操作执行开始');
3 requestIdleCallback(() => {
4 console.log('执行栈:requestIdleCallback');
5 });
6 setTimeout(() => {
7 console.log('执行栈:setTimeout');
8 }, 0);
9 requestAnimationFrame(() => {
10 console.log('执行栈:requestAnimationFrame');
11 });
12 Promise.resolve().then(() => {
13 console.log('执行栈:Promise');
14 });
15 for (let i = 0; i < 100000; i++) {} // 同步耗时阻塞
16 console.log('执行栈:慢操作执行结束');
17}
18slowAction();
🖥️ 控制台输出 (console.log 真实先后顺序)0 logs
// 暂无输出,点击「下一步」观察打印顺序
🥞 1. 执行栈 Call Stack (光杆站长工作台 · LIFO)正在执行
[执行栈已清空 — 准备检查微任务队列]
🚄 2. 微任务队列 Microtask Queue (特快列车 · 栈空必先全清)Priority #1
[队列为空]
🎨 3. 渲染帧回调 requestAnimationFrame (UI 重绘前执行)Before Repaint
[队列为空]
🚂 4. 宏任务队列 Macrotask Queue (普通列车 · 每轮仅取一个)Priority #2
[队列为空]
☕ 5. 空闲回调 requestIdleCallback (站长彻底空闲时触发)Lowest Priority
[队列为空]
💡 省钱实战方案 · 开发者精选资源官方合作链接 · 由对应平台提供全部售后保障

从“光杆站长”心智模型彻底攻克 JS 事件循环与异步队列优先级

本沙盒与站内高人气博文《一文讲透事件循环》深度联动。你可以把 JavaScript 单线程主引擎想象成火车站里同一时刻只干一件事的光杆站长:

  • 日常事务(执行栈 Call Stack): 站长手头正在执行的同步语句,任何列车进站都必须等他把手头这段同步代码跑完。
  • 特快列车(微任务队列 Microtask Queue): Promise.then、queueMicrotask、MutationObserver。只要执行栈一清空,站长必须把所有特快列车全部接完(哪怕接车中途又开来新的特快列车)。
  • 重绘检票(requestAnimationFrame): 在浏览器下一帧 UI 渲染重绘之前触发,适合高帧率动画计算。
  • 普通列车(宏任务队列 Macrotask Queue): setTimeout、setInterval、DOM 事件、I/O 回调。每次事件循环只从队头取出一列执行,执行完后立即回头检查微任务队列。
  • 空闲巡检(requestIdleCallback): 只有当站长把特快车、重绘和普通列车全忙完了,处于空闲状态时才会顺手处理。

常见问题与实战解答 (FAQ)

Q1 为什么微任务(Promise.then)总是比宏任务(setTimeout 0ms)先执行?
根据 HTML 标准事件循环规范,每当主线程执行栈(Call Stack)清空后,引擎会立即进入微任务检查点(Microtask Checkpoint),把微任务队列中的所有任务(包括执行期间新衍生的微任务)全部取出来连续执行完毕,直到微任务队列彻底为空,才会去进行 UI 渲染判断或从宏任务队列取出下一个宏任务。
Q2 requestAnimationFrame 和 requestIdleCallback 分别在事件循环的什么时机触发?
•
requestAnimationFrame (rAF) 属于浏览器 UI 渲染管线阶段(通常按屏幕刷新率 60Hz 即约 16.6ms 触发一次),在微任务清空之后、浏览器执行 Layout 与 Paint 重绘之前调用。
•
而 requestIdleCallback 仅在当前帧的同步代码、微任务、重绘以及常规宏任务全部处理完且仍有剩余空闲时间时,才由主线程兜底调用。
Q3 async / await 在事件循环中是如何被拆解的?
•
async 函数在遇到 await 关键字之前的代码是完全同步执行的(立即入栈运行)。
•
当执行完 await 右侧的表达式后,引擎会让出当前协程的执行权,并将 await 下方的剩余代码包装进一个微任务(等同于 Promise.resolve().then(...))排入微任务队列。