⚡ 编程实验室🏗️ HTML🎨 CSS⚡ JavaScript🐍 Python🗄️ SQL☕ Java⚛️ React💚 Vue🟢 Node.js⚙️ C语言🐘 PHP🐹 Go🔷 TypeScript🐬 MySQL🔧 C++🎯 C#🦀 Rust🅱️ Bootstrap💡 jQuery🎸 Django🍃 MongoDB👗 Sass🎪 Kotlin📊 R语言📋 XML📊 Excel🐘 PostgreSQL🐳 Docker🅰️ Angular🎮 游戏🏠 网站首页

JavaScript 事件循环完全理解

彻底搞懂 Event Loop、Call Stack、微任务和宏任务,面试不再慌。 · 难度:进阶 · +20XP

为什么 setTimeout 0 不是立即执行?

你一定见过这段面试题:

console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出:1 → 3 → 2

如果你以为 setTimeout(fn, 0) 会"立即"执行,那输出应该是 1→2→3。但实际是 1→3→2。为什么?答案就在事件循环里。

JavaScript 是单线程的

JS 引擎只有一个调用栈(Call Stack),一次只能做一件事。你可以把它想象成一个只有一部电梯的大楼——一次只能送一个人,后面的人必须排队。

function a() { b(); }
function b() { c(); }
function c() { console.log('到顶楼了'); }
a();

调用栈的变化:

  1. 调用 a() → 栈: [a]
  2. a 里调用 b() → 栈: [a, b]
  3. b 里调用 c() → 栈: [a, b, c]
  4. c 执行完返回 → 栈: [a, b]
  5. b 返回 → 栈: [a]
  6. a 返回 → 栈: [](清空)

Web APIs 和任务队列

但 JavaScript 不只是调用栈。浏览器还提供了 Web APIs(setTimeout、fetch、DOM 事件等)和任务队列(Task Queue)

完整的流程图:

  1. 同步代码 → 进入调用栈 → 立即执行
  2. 遇到 setTimeout → 交给浏览器的定时器线程 → 定时器到期后,回调进入宏任务队列
  3. 遇到 Promise.then → 回调进入微任务队列
  4. 调用栈清空后 → 先清空微任务队列 → 再从宏任务队列取一个执行 → 重复

现在回到开头的例子,答案就清楚了:

console.log('1');        // ① 同步,立即输出 1
setTimeout(() => console.log('2'), 0); // ② 交给定时器,0ms 后回调进入宏任务队列
console.log('3');        // ③ 同步,立即输出 3
// ④ 调用栈空了 → 微任务队列也是空的 → 从宏任务队列取出 setTimeout 回调
// ⑤ 输出 2

微任务 vs 宏任务

类型包含优先级
微任务 (Microtask)Promise.then/catch/finally、queueMicrotask()、MutationObserver🔥 高(每轮事件循环先清空)
宏任务 (Macrotask)setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染低(每轮只取一个)

经典面试题:

console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// 输出:start → end → promise → timeout

解释:Promise.then 是微任务,优先于 setTimeout(宏任务)。所以 promise 在 timeout 之前输出。

进阶:微任务中的微任务

Promise.resolve().then(() => {
  console.log('1');
  Promise.resolve().then(() => console.log('2'));
});
Promise.resolve().then(() => console.log('3'));
// 输出:1 → 3 → 2

关键规则:事件循环会清空所有微任务,包括在微任务执行过程中新添加的微任务。所以 1 输出后,3 已经在队列里了,先输出 3,再输出 2。

requestAnimationFrame 的位置

requestAnimationFrame(rAF)在渲染之前执行。完整的执行顺序:

  1. 执行一个宏任务
  2. 清空所有微任务
  3. 执行 rAF 回调
  4. 浏览器渲染
  5. 回到步骤 1

动手试试

  1. 基础练习:写一段代码,用 setTimeout 实现"每隔 1 秒输出一个数字"的计数器,从 1 到 5
  2. 进阶应用:写一段代码包含 2 个 setTimeout、3 个 Promise.then、1 个同步 console.log,预测输出顺序,然后运行验证
  3. 项目实战:在你的项目中找到一段加载数据后更新 DOM 的代码,用 queueMicrotaskPromise 确保数据更新在 DOM 渲染前完成

接下来学什么?

掌握了事件循环,下一课学习 JavaScript Promise 从入门到精通——理解 then/catch/finally 的链式调用、Promise.all/race/allSettled 的实战场景。

📖 译自 freeCodeCamp JavaScript Event Loop Explained (CC BY 4.0),有改编和扩充。

🚀 升级VIP
解锁全部课程+AI助手

🏆 学习排行

加载中...

📊 统计

📖 231 篇
0 完成
🔥 0