跳到主要内容

事件循环与任务队列

JavaScript 同一时刻只跑一段用户代码(单线程)。异步不是「多线程一起算」,而是:当前栈跑完 → 从队列里取下一个任务。浏览器里线程怎么拆,见 浏览器架构;本篇只讲 JS 能观察到的调度。

Promise / async 怎么进微任务,见 Promise 与异步错误。Node 与浏览器宿主差在哪,见 Node 与浏览器运行时。

解决什么问题​

痛点事件循环怎么解释
setTimeout(fn, 0) 不是立刻跑定时器回调进宏任务,要等当前栈 + 微任务清空
Promise.then 比 setTimeout 先then 进微任务,同一轮里微任务排在宏任务前
长循环卡住点击 / 渲染JS 跑着时渲染插不进去;栈不空就没有下一帧
面试「输出顺序」先同步,再微任务,再宏任务;看清第几轮

一句话:调用栈执行当前任务;事件循环在栈空时,先清空微任务,再取一个宏任务。

三件套​

调用栈(Call Stack) 正在执行的函数帧
宏任务队列(Task / Macrotask) script、setTimeout、setInterval、I/O、UI 事件
微任务队列(Microtask) Promise.then/catch/finally、queueMicrotask、MutationObserver

浏览器里还有 requestAnimationFrame(偏渲染帧,不要和 setTimeout(0) 混为一谈)以及 requestIdleCallback(空闲时,兼容性别当主路径)。

经典顺序(同一轮):

console.log("1 sync");

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

Promise.resolve().then(() => console.log("3 microtask"));

console.log("2 sync");
// 1 sync → 2 sync → 3 microtask → 4 timeout

一轮长什么样​

1. 取出一个宏任务(第一次是整份 script)
2. 跑完调用栈
3. 清空微任务队列(期间 then 又 enqueue 的微任务也要跑完)
4. 必要时渲染
5. 取下一个宏任务 → 回到 2

await 后面的代码等价于 then 回调,因此也是微任务。不要背「await 是宏任务」。

async function f() {
console.log("a");
await null;
console.log("c");
}
f();
console.log("b");
// a → b → c

和浏览器线程的关系​

浏览器架构 里:定时器在计时线程到期后,把回调丢进任务队列;网络在请求线程完成后同样入队。真正执行回调的仍是 JS 引擎线程。

所以:

  • setTimeout 的延迟是「最早何时入队」,不是「保证此时执行」
  • 主线程被 while (true) 占满时,到期的定时器也跑不了
  • HTML 规定嵌套 setTimeout 有 4ms 下限,0 不是 0

Node 的阶段直觉(面试够用)​

浏览器用「宏/微」就够答。Node 的 event loop 还分 阶段(简化):

阶段典型来源
timerssetTimeout / setInterval
pollI/O 回调(读文件、socket)
checksetImmediate
closesocket 关闭等

process.nextTick 不是 普通微任务:它比 Promise.then 更早、在当前操作之后立刻排空。生产里少用 nextTick 递归,容易饿死 I/O。

setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("then"));
process.nextTick(() => console.log("tick"));
// 常见:tick → then → timeout/immediate(后两者谁先与进入 loop 的时机有关,别赌死顺序)

排障口令​

现象先怀疑
页面掉帧、点击延迟同步长任务;用拆分 / Worker,而不是再套一层 setTimeout 当多线程
「定时器不准」主线程忙;延迟只是入队下限
Promise 里套 Promise,日志顺序乱先画栈,再标微任务轮次
Node 里 nextTick 循环占满微任务/nextTick 饿死 poll

要点​

  1. 单线程 = 同一时刻只有一个调用栈;异步靠队列
  2. 每轮:一个宏任务 → 清空微任务 →(浏览器)渲染
  3. then / await 后续是微任务;setTimeout 是宏任务
  4. Node 多阶段 + nextTick 优先;不要用「浏览器口令」硬套 setImmediate 顺序

面试速记​

  1. 为什么 JS 是单线程还能异步? 执行线程只有一条;I/O 和定时器在宿主完成后再把回调入队,由事件循环调度。
  2. 微任务和宏任务谁先? 当前宏任务结束后先清空微任务,再取下一个宏任务。
  3. setTimeout(fn, 0) 做什么? 尽快作为下一个宏任务跑,不是插队到同步代码前面。
  4. 这段 console 顺序怎么排? 先同步,再 then/await,再 setTimeout;嵌套 then 仍在微任务队列里继续排。
  5. 卡死页面的本质? 调用栈不空,渲染和下一轮任务都进不来。