写给零基础:JavaScript 事件循环到底在转什么
约 1 分钟· 656 字 156 次阅读
"JavaScript 是单线程的,但它不会卡死"——这句话背后就是事件循环。理解它,才能真正搞懂 setTimeout、Promise、async/await 的执行顺序。
单线程为什么能异步
JS 只有一个主线程,但它把任务分成了两类:
- 同步任务:立即执行,排队挨个来;
- 异步任务:先交给环境(浏览器/Node),完成后再回到主线程执行。
这个"完成后再回来"的机制,就是事件循环。
宏任务与微任务
异步任务又分两层:
| 类型 | 代表 | 优先级 |
|---|---|---|
| 宏任务(macrotask) | setTimeout、setInterval、I/O |
低 |
| 微任务(microtask) | Promise.then、queueMicrotask、async/await |
高 |
规则只有一条:每一轮事件循环,先清空所有微任务,再执行一个宏任务。
三道题验证理解
第一题:基础顺序
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// 输出:A D C B
为什么 C 在 B 前面?因为 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 已经在下一层的微任务队列里,所以先 3 后 2。
第三题: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 之后才异步 |
| 微任务无限添加会卡死 | 会,微任务队列每轮必须清空,死循环会阻塞渲染 |
小结
事件循环的钥匙就两句话:
- 微任务优先于宏任务,每轮先清微任务再取宏任务;
await是微任务的语法糖,理解了微任务就理解了async/await。
把这三道题跑一遍,你就能在面试或调试中稳稳预判执行顺序。