01事件循环与单线程

JS 是单线程的,为什么页面还能同时下载图片、响应点击、跑动画? 答案是事件循环(Event Loop)。本课把调用栈、宏任务、微任务、 渲染时机一次讲透——这是理解 Promise / async / fetch 的总开关。

约 14 分钟 3 个动手实验 异步总纲

一、单线程:一次只能做一件事

浏览器里 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 的间隔; ③ 解释为什么微任务死循环也会卡死页面。