01事件循环与单线程
JS 是单线程的,为什么页面还能同时下载图片、响应点击、跑动画? 答案是事件循环(Event Loop)。本课把调用栈、宏任务、微任务、 渲染时机一次讲透——这是理解 Promise / async / fetch 的总开关。
一、单线程:一次只能做一件事
浏览器里 JS 引擎(如 V8)只有一个调用栈,同一时刻只执行一段代码。
如果同步代码里有个 while(true){},整个页面就卡死——按钮点不动、动画不动。
同步阻塞 = 页面卡死。所以耗时操作(网络、定时、大计算)
必须异步:先挂起,等结果好了再排队回来执行。
二、事件循环的「四层」模型
① 调用栈
同步代码在这里执行;函数调用压栈,执行完弹栈
② 微任务
Promise.then / catch / finally、queueMicrotask、MutationObserver —— 优先清空
③ 宏任务
setTimeout / setInterval / I/O / UI 事件 / MessageChannel —— 每次循环取一个
④ 渲染
浏览器找机会重绘(不是每帧都有);然后进入下一轮循环
一轮循环的顺序:清空调用栈 → 清空所有微任务 →(可能渲染)→ 取一个宏任务执行 → 再清空微任务 → ……
经典输出顺序题:点按钮跑 5 个 console,对照解释看宏/微任务谁先谁后
点按钮看结果
三、宏任务 vs 微任务
| 宏任务 Macrotask | 微任务 Microtask | |
|---|---|---|
| 例子 | setTimeout、setInterval、I/O、UI 事件、requestAnimationFrame* | Promise.then/catch/finally、queueMicrotask、MutationObserver |
| 执行时机 | 每轮循环取一个 | 当前宏任务结束后全部清空 |
| 优先级 | 低 | 高(插队到下一个宏任务之前) |
| 典型用途 | 延迟执行、周期任务、事件回调 | 状态更新后的后续逻辑、Promise 链 |
requestAnimationFrame 严格说在渲染前跑,比 setTimeout 更「贴帧」,
做动画用 rAF,不用 setInterval。四、setTimeout(fn, 0) 也不「立刻」
延迟写 0,也至少要等:当前同步代码跑完 → 微任务清空 → 浏览器到了「可以处理宏任务」的时候。 而且浏览器对嵌套 setTimeout 有最小 4ms 的节流(5 层嵌套以后)。
timeout 0 的真实延迟:连续嵌套 setTimeout,看间隔被拉长到约 4ms
点按钮
五、渲染与事件循环的关系
- JS 同步跑太久,浏览器没机会渲染——掉帧、输入卡顿。
- 微任务太多也会堵渲染(一个 then 里再 queueMicrotask 死循环 = 同样卡死)。
- 长计算切片:
setTimeout分片、requestIdleCallback、Web Worker 把计算丢出去。
六、和闭包 / 异步 API 的连接
- 闭包:回调被挂到任务队列时,用闭包记住外层变量。
- Promise:微任务的代表,下一课专讲。
- async/await:本质是 Promise + 生成器,仍走微任务。
- fetch:网络 I/O 是宏任务来源,完成后 resolve 进微任务。
七、常见面试题模板
八、本章小结
- JS 单线程:一次一个调用栈,靠事件循环实现异步。
- 一轮循环:清空同步栈 → 清空微任务 →(渲染)→ 一个宏任务。
- 微任务(Promise.then)优先于宏任务(setTimeout)。
setTimeout 0不立刻跑;嵌套后有 4ms 节流。- 长同步代码会堵渲染——切片或丢 Worker。
- 做动画用
requestAnimationFrame,别用 setInterval。
动手练习:
① 写一段代码,让
setTimeout、Promise.then、同步 console 交错,预测输出再验证;
② 用 performance.now() 测 10 次连续 setTimeout 0 的间隔;
③ 解释为什么微任务死循环也会卡死页面。