你以为
setTimeout(fn, 0)就是"立刻执行"?你以为 JavaScript 是"单线程所以跑不动并发"?你以为await真的会"暂停"整个函数?——这些全是被简化口号喂大的误解。本文拆解事件循环(Event Loop)的真实运作方式:异步从来不需要多线程,它需要的是一套精密的排队规则。
In The Loop 是 Google 工程师 Jake Archibald 在 JSConf EU 的一场演讲,被公认为"全网讲事件循环讲得最透"的视频。它之所以常年出现在每一个"前端必看清单"里,不是因为它有多前沿,而是因为它戳破了三个流传最广的半真半假说法:"JS 是单线程""异步就是多线程""setTimeout(fn,0) 等于马上执行"。
这俩东西为什么重要?因为 90% 的"页面卡死""动画掉帧""Promise 链阻塞渲染""后台标签页定时器失灵"这类诡异 bug,根子都不在业务逻辑,而在你对事件循环调度顺序的理解是错的。读懂这一套规则,你会发现那些"玄学"全是有解的因果链。本文不仅复述视频的核心论点,还会补上规范里的具体数字(4ms、1000ms、16.7ms 这些阈值从哪来),以及 Node.js 端的差异——这些是视频为了通俗而省略的部分。
| 项目 | 内容 |
|---|---|
| 视频标题 | In The Loop(在循环之中) |
| 演讲者 | Jake Archibald(Google Chrome 团队) |
| 频道 / 来源 | JSConf EU |
| 时长 | 约 37 分钟 |
| 播放量 | 百万级,长期稳居"前端必看"推荐榜首 |
| 核心主题 | 浏览器事件循环、microtask/macrotask/render 三条队列的调度规则 |
说明:本文撰写时联网检索工具配额已耗尽,无法实时核验最新播放量。该演讲为该领域公认标杆,分析基于其公开内容与 WHATWG HTML 规范交叉验证。
这句口号的最大害处,是让人以为整个浏览器只有一个线程。真相是:浏览器是一个多进程、多线程的应用,JS 引擎(V8/SpiderMonkey)只是其中的一个租户,被钉死在主线程(main thread)上跑。
主线程之外,浏览器至少还养着这些线程:
setTimeout 的计时根本不是 JS 数的,是这个原生线程在数所以当一个 fetch 请求"异步"发出时,主线程只是把活儿外包给网络线程,自己拍拍屁股继续往下跑。等网络线程把响应拿回来,它不会直接冲进主线程(那会撞上正在执行的同步代码),而是把回调塞进一个队列,等主线程闲下来再处理。这就是"单线程异步"的全部秘密:不是主线程自己同时干多件事,而是主线程只管调度,脏活累活全外包,结果靠队列回流。
很多人把"异步"和"并发/并行"混为一谈。视频反复强调:异步是一种调度策略,并行是一种执行能力,两者是正交的。
一个反直觉的事实:单线程完全可以做到异步。你去餐馆点菜,服务员(主线程)把单子递给厨房(其他线程/系统)后立刻去接待下一桌,而不是傻站着等你的菜——这就是异步。菜好了不是直接端到你桌上(打断你正在进行的对话),而是放在"出餐口"(队列),服务员空了才端。
真正决定"会不会卡"的,是主线程上跑的同步代码有多重。一段纯同步的 while(true) 死循环,能瞬间把整个页面冻成截图——这跟线程数毫无关系,因为主线程被占着,事件循环根本转不起来,所有点击、动画、定时器回调全都被堵在队列里出不来。
把那些花哨的图剥掉,事件循环的内核就是个死循环(伪代码):
while (true) {
1. 取一个 macrotask(也叫 task),执行它
2. 清空整个 microtask 队列(全部执行,包括执行中新增的)
3. 检查是否到了该渲染的时刻
- 如果是:跑 requestAnimationFrame 回调 → 渲染(style/layout/paint)
- 如果否:跳过
4. 回到第 1 步
}
注意三个关键细节:
第一,microtask 是"清空",macrotask 是"取一个"。 每轮循环只处理一个 macrotask,但会把 microtask 队列全部榨干——包括这些 microtask 执行过程中新冒出来的 microtask。这就是那句著名的执行顺序口诀:
同步代码 →(清空所有)Microtask →(取一个)Macrotask →(清空所有)Microtask →(取一个)Macrotask → …
哪些是 microtask?Promise 的 .then/.catch/.finally、queueMicrotask()、await 后面的部分、MutationObserver、Node 的 process.nextTick(甚至比 microtask 还优先)。哪些是 macrotask?setTimeout/setInterval、用户事件回调(click)、网络回调、MessageChannel、I/O。
第二,渲染不是每轮都有。 浏览器渲染受限于显示器刷新率,通常约 60Hz,也就是一帧 16.7ms。事件循环不会"每轮都画",而是当它判断"离上次渲染够久了"才画。这是后面讲动画为什么该用 requestAnimationFrame 的根本原因。
第三,microtask 在渲染之前。 这意味着:如果你在一个 macrotask 里塞了一万个 Promise.resolve().then(...),它们会在浏览器有机会渲染之前全部跑完——页面直接卡死,用户看到的就是冻结。这是 microtask 最危险的特性:它没有让渡控制权的机制。
setTimeout(fn, 0) 从来不是 0 毫秒这是视频里最"打脸"的一段。无数教程教你"想立刻执行就用 setTimeout(fn, 0)",但规范(WHATWG HTML 标准)白纸黑字写了三个例外:
setTimeout 被嵌套调用达到 5 层及以上时,最小延迟被强制提升到 4ms。所以你递归 setTimeout 想做高频轮询,第 5 次开始每次至少等 4ms。pageVisible 相关策略的定时器最小延迟拉到 1000ms。这就是为什么你切到别的标签页,那个"每秒动一次"的动画会变成"每秒动一次但跟瞎了似的"。所以 setTimeout(fn, 0) 的真实语义是"尽快,但不早于当前 macrotask 结束后的下一轮,且不早于规范规定的下限"。它从来不是"0ms"。
await 不是暂停,是"分家"很多人把 await 理解成"让整个函数睡一会儿"。错。async function 在编译期就被改写成一个状态机(在 V8 里是显式的 generator 状态机),await 的作用是:保存当前状态、退出函数、把后续代码注册为一个 microtask,等 Promise 决议后再回来继续。
也就是说,await 这一行之后的代码,等价于包在 Promise.resolve(x).then(...) 里——它是 microtask。这就是为什么这段代码:
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
(async () => { await Promise.resolve(); console.log(4); })();
console.log(5);
// 输出顺序:1, 5, 3, 4, 2
3 和 4(microtask)永远抢在 2(macrotask)前面。理解了"await 本质是 then",所有异步顺序题都能推出来,不用背。
requestAnimationFrame视频最后落到一个实战结论:做动画别用 setTimeout/setInterval,用 requestAnimationFrame(rAF)。
原因就藏在前面那张调度图里:rAF 的回调跑在渲染之前的那个步骤。这意味着:
setTimeout 还在傻转。setInterval(fn, 16) 做动画的常见翻车:回调可能在某一帧里跑了两次、或在屏幕刷新的间隙跑、在后台狂跑耗电。rAF 从机制上消灭了这些问题。
视频主要讲浏览器,但值得补一嘴 Node.js。Node 用的是 libuv,事件循环分成 6 个阶段:timers → pending callbacks → idle/prepare → poll → check(setImmediate)→ close callbacks,循环往复。
理念同源(都是"队列 + 调度"),但细节不同:Node 的 process.nextTick 优先级高于所有 microtask(这点常被当作面试陷阱);setImmediate(check 阶段)和 setTimeout(fn,0)(timers 阶段)的先后,取决于主模块还是 I/O 回调里调用,并不固定。同一个 Promise.then,在 Node 11 之前是"每个 macrotask 后清一次 microtask",11 之后改成和浏览器一致的"每个宏任务/阶段回调后都清"——这是为了行为对齐,也说明"浏览器规范"才是事实标准。
flowchart TD
A[事件循环 Event Loop<br/>一个 while 死循环] --> B[① 取一个 Macrotask 执行]
B --> C[② 清空整个 Microtask 队列<br/>含执行中新产生的]
C --> D{③ 是否到了<br/>该渲染的时刻?}
D -- 是 --> E[跑 requestAnimationFrame 回调]
E --> F[渲染: style → layout → paint]
D -- 否 --> B
F --> B
G[主线程 JS 引擎] -.外包.-> H[网络/GPU/定时器<br/>等独立线程]
H -- 结果回流--> Q1[Macrotask 队列]
P[Promise.then / await后续<br/>queueMicrotask] --> Q2[Microtask 队列]
Q1 --> B
Q2 --> C
style A fill:#ffd966,stroke:#b8860b,stroke-width:2px
style C fill:#ffcccc,stroke:#cc0000
style F fill:#ccffcc,stroke:#008800
第一,永远别在同步代码里塞海量 microtask。 一旦你写了"递归 Promise.resolve().then 干重活"的代码,页面会冻结到它跑完——因为 microtask 不会让出控制权给渲染。真要做后台重活,用 setTimeout(fn, 0) 切片、或用 requestIdleCallback、或直接甩给 Web Worker。
第二,永远别用 setInterval 做动画。 改成 requestAnimationFrame,立刻获得刷新率对齐 + 后台自动暂停 + 帧内连贯渲染三件套。
第三,理解了"microtask 永远插队"就能调试一切"回调顺序怪异"。 记住顺序口诀:同步 → microtask(全清)→ macrotask(一个)→ microtask(全清)→ …。遇到面试题或线上 bug,把每段代码归类,按口诀推一遍,答案自现。
第四,对性能敏感的逻辑,要意识到"后台标签页的定时器被砍到 1Hz"。 别指望切到后台的页面还能靠 setInterval 做精确计时或动画——用可见性 API(visibilitychange)配合时间戳差值来补偿。
第五,async 函数不是免费的并行。 多个 await 串行写会变成串行等待,白白浪费时间;想并行要用 Promise.all([a(), b()])。理解 await 是"把后续代码挂成 microtask"后,这个优化就显而易见了。
第六(认知升级),异步是"调度问题"而非"线程问题"。 这是最值钱的一条。一旦你把脑子从"开更多线程"切到"更聪明地排队列、外包、让出控制权",你写出的前端代码会少 80% 的卡顿 bug。这条原则甚至能迁移到后端:Node 的非阻塞 I/O、Go 的 goroutine 调度、Nginx 的事件驱动,本质上都是"少量线程 + 队列调度"打赢"一个连接一个线程"。
https://html.spec.whatwg.org/multipage/webappapis.html#event-loopshttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoophttps://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/nextTick/setImmediate 细节差异。异步从来不靠多线程,靠的是"外包 + 三条队列 + 一个 while 循环"——读懂事件循环,前端 80% 的卡顿和诡异回调顺序都有了解释。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。 完成摘要
下一步建议
1. 待联网配额恢复(7/18)后,可补一次实时检索核验最新播放量并探索当周真正爆款,作为后续轮次素材库
2. 如需制作配套视频,可调用 content-to-video skill 将本文转为带 Mermaid 图的讲解视频
3. 本文 Mermaid 图可用 mermaid_save 导出为 SVG/PNG 嵌入公众号
注意事项 - 因无法实时联网,"播放量百万级"为基于该演讲长期公开数据的描述,未做精确到万的核验,已在文中诚实标注 - 视频为 2018 年内容,"本周热门"的时效性较弱;若后续轮次需严格时效,建议在配额恢复后重选
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
