POST://javascript-event-loop
带点橘子味的馒头的头像
带点橘子味的馒头

FRONTEND / DESKTOP DEV

返回文章列表

写给零基础:JavaScript 事件循环到底在转什么

1 分钟· 656 156 次阅读

"JavaScript 是单线程的,但它不会卡死"——这句话背后就是事件循环。理解它,才能真正搞懂 setTimeoutPromiseasync/await 的执行顺序。

单线程为什么能异步

JS 只有一个主线程,但它把任务分成了两类:

  • 同步任务:立即执行,排队挨个来;
  • 异步任务:先交给环境(浏览器/Node),完成后再回到主线程执行。

这个"完成后再回来"的机制,就是事件循环。

宏任务与微任务

异步任务又分两层:

类型 代表 优先级
宏任务(macrotask) setTimeoutsetInterval、I/O
微任务(microtask) Promise.thenqueueMicrotaskasync/await

规则只有一条:每一轮事件循环,先清空所有微任务,再执行一个宏任务

三道题验证理解

第一题:基础顺序

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// 输出:A D C B

为什么 CB 前面?因为 Promise 是微任务,setTimeout 是宏任务——微任务永远先清空。

第二题:嵌套微任务

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

同一轮里,第一个 .then 队列先执行 1,又追加了 2 的微任务;但 3 已经在下一层的微任务队列里,所以先 32

第三题:async/await 的本质

async function foo() {
  console.log("a");
  await bar();
  console.log("b");
}
async function bar() {
  console.log("c");
}
foo();
console.log("d");
// 输出:a c d b

await 会把后面的代码包进微任务:foo() 执行到 await 暂停,bar() 同步执行完 c,主线程继续打印 d,最后微任务恢复打印 b

为什么 setTimeout 不准

setTimeout(fn, 0) 不是"0 毫秒后执行",而是"至少 0 毫秒后,放进宏任务队列"。如果前面有大量微任务或同步任务,它就得等。所以:

const t0 = Date.now();
setTimeout(() => console.log(Date.now() - t0), 0); // 可能是 4ms 甚至更多
while (Date.now() - t0 < 100) {} // 阻塞主线程 100ms
// 输出:>100

常见误区

误区 正解
setTimeout(0) 一定最先 微任务优先,Promise 先于它
async 函数是异步的 函数体同步执行,await 之后才异步
微任务无限添加会卡死 会,微任务队列每轮必须清空,死循环会阻塞渲染

小结

事件循环的钥匙就两句话:

  1. 微任务优先于宏任务,每轮先清微任务再取宏任务;
  2. await 是微任务的语法糖,理解了微任务就理解了 async/await

把这三道题跑一遍,你就能在面试或调试中稳稳预判执行顺序。

相关文章