首页 › programming › 单线程撑起整个Web:事件循环神课

单线程撑起整个Web:事件循环神课深读

导语:一场把“看不见的调度”画出来的演讲

你有没有想过一个奇怪的问题:JavaScript 是单线程的,为什么整个 Web 世界没有因为它“一次只能干一件事”而卡成一锅粥?你点按钮、发请求、收消息,页面丝滑得像多线程一样。这背后藏着前端工程最核心的运行时机制——Event Loop(事件循环)。

把这件事讲得最透彻的,是 Philip Roberts 在 JSConf EU 2014 上的演讲《What the heck is the event loop anyway?》(事件循环到底是个啥)。十多年过去,这场演讲的播放量早已是百万级,至今仍被各大编程课程、面试题库反复引用。它最了不起的地方不是“讲了很多”,而是讲得极少、画得极清:全场只围绕三个盒子——调用栈、Web APIs、回调队列——外加一个转圈的守门员,就把 JavaScript 的异步模型钉死在了一张图上。

演讲者还顺手开源了演示工具 Loupe:把任意一段 JS 代码丢进去,你能亲眼看着回调函数飞出主线程、排队、再飞回来。对任何被 setTimeout、Promise、async/await 的执行顺序折磨过的开发者来说,这26分钟是一次认知校准:你以为的异步,和引擎眼里的异步,可能根本不是一回事。本文带你逐段拆解它,并补上它没讲到的下半场——微任务。

视频背景卡片

项目 内容
视频标题 What the heck is the event loop anyway?
演讲者 Philip Roberts(英国工程师,Loupe 作者)
频道 JSConf(JSConf EU 2014 现场)
时长 约26分钟
播放量 百万级,前端领域常青经典
核心主题 JavaScript 单线程模型、调用栈、回调队列与事件循环
配套工具 Loupe 可视化工具(latentflip.com/loupe)

核心内容拆解

1. “单线程”的准确含义:一次只有一叠账

演讲开场,Philip 给出了那句被引用了十年的原话,大意是:“JavaScript 是单线程的:只有一个调用栈(call stack),一个堆(heap)。” 这句话常被误解,值得掰开。所谓单线程,不是指这门语言在任何意义上都只有一个线程(浏览器里显然还有渲染线程、合成线程等),而是指你的 JS 代码在主线程上一次只能执行一件事,靠一个调用栈来记账:调用函数就压栈,返回就出栈,栈顶永远指向“正在执行的那一帧”。

视频里有个现场翻车演示:他写了一个没有终止条件的递归函数,调用栈一帧帧往上摞,直到超出上限,浏览器直接抛出经典的 Maximum call stack size exceeded——这就是“栈溢出”这个词的现场版。延伸分析:理解“一叠账”这个心智模型特别重要,它意味着 JS 里不存在两个函数“同时执行到一半”的情况,你的代码天然没有交错执行的问题——这是后面一切设计的起点。

2. 阻塞有多疼:一段慢代码能冻住整个页面

既然一次只做一件事,那就引出了这场演讲的核心焦虑词:阻塞(blocking)。Philip 演示了一段“生病”的代码:页面里放了个按钮,然后主线程跑一个耗时的同步循环(比如模拟一个同步的网络请求)。结果是什么?按钮渲染不出来、点不了、点了也没反应——因为渲染也是要“排队用”主线程的,调用栈不空,浏览器根本插不进手。

他特别点名了当年真实存在过的罪魁祸首:同步 AJAX。一个慢接口能让整张页面假死十几秒。延伸分析:这正是今天前端性能指标越来越看重“主线程占用”的历史根源。Google 从2024年3月起把 INP(Interaction to Next Paint,交互到下一次绘制的延迟)定为 Core Web Vitals 核心指标,本质上就是在度量“你有没有把主线程霸占太久”——2014年这场演讲里的担忧,十年后成了行业标准。哪些算阻塞?大 JSON 的同步解析、无节制的同步循环、同步弹窗 alert(),全都算。

3. 全场最大反转:setTimeout 根本不是 JavaScript 的

这是整场演讲的认知爆点,也是最能刷新面试观感的部分。Philip 问:当你在浏览器里调用 setTimeout,是谁在计时?答案令人意外——不是 V8。V8 这类 JS 引擎的职责清单其实短得可怜:执行代码、维护调用栈和堆。仅此而已。计时器、DOM 事件、XMLHttpRequest/fetch,这些“异步能力”全部是浏览器提供的 Web APIs,它们活在 JS 引擎之外。

也就是说:setTimeout(fn, 5000) 这行代码,实际上是“委托”浏览器去计时,5秒后浏览器把 fn 这个回调放进回调队列等着。同样的分工在 Node.js 里换成了 libuv 和操作系统。延伸分析:这个分层视角把很多“玄学”一次性祛魅了——为什么不同浏览器的 setTimeout 最低延迟历史行为有差异、为什么 Node 的 setTimeout 语义要单独查文档,根源都在于“异步设施不是语言本身,而是宿主环境的公共设施”。语言只负责同步执行,等待和排队全部外包。

4. 事件循环的正式定义:一句话就能说完

三个盒子齐了:调用栈、Web APIs、回调队列(callback queue / task queue)。那事件循环是什么?Philip 给出的定义简单到几乎不像“高级知识”:事件循环就是浏览器里的一个小守卫,它的全部工作就是盯着调用栈和回调队列——一旦发现调用栈空了,就把队列里的第一个回调拿起来,压到栈上执行。 完了,就这么点事。

但他紧接着点破了这个模型的精妙:正因为有这个“栈空才搬”的铁律,回调永远不会打断你正在执行的代码。任何一段同步代码跑完之前,队列里排再多的回调也只能干等。这给了 JS 一个多线程语言梦寐以求的性质——同一时刻永远只有一段代码在碰数据,DOM 永远不会被两个执行流同时改花。不需要锁,没有死锁,秩序是排队排出来的。延伸分析:我个人的观点是,这是 JS 历史上最划算的一次“以简驭繁”:它用“等待外包 + 单点调度”换掉了整个锁体系,代价只是开发者必须建立新的心智模型——而这正是这场演讲存在的原因。

5. setTimeout(fn, 0):零毫秒也要排队

理解了守卫规则,一个经典谜题自动解开:setTimeout(fn, 0) 为什么不立刻执行?视频里的演示非常直观——即便延迟设为0,回调也必须先被丢进队列,然后等当前调用栈彻底走完,事件循环才有资格把它搬上栈。所以它保证的不是“0毫秒后执行”,而是“当前这批代码跑完后尽早执行”。HTML 规范后来甚至明确规定嵌套定时器有最小延迟限制(常见为4毫秒),“0毫秒”从来就不是一个能兑现的承诺。

Philip 现场用 Loupe 跑了一段混合代码:同步的 console.log、setTimeout 回调、一次性的点击事件回调,三种颜色的小方块在屏幕上飞来飞去——同步的直接上栈执行完消失,异步的先飞出主线程、稍后落进队列、再在栈空的瞬间飞回栈上。整场演讲最值钱的30秒就在这里:异步不再是一个抽象名词,而是一次看得见的搬家过程。

6. 视频没讲的下半场:微任务改变了排队规则(延伸分析,非视频内容)

必须说明:2014年的这场演讲只讲了宏任务队列,而 ES6 普及 Promise 之后,世界变了。今天真实的浏览器里有两个队列:宏任务队列(setTimeout、事件回调)和微任务队列(Promise.then、await 之后的代码、queueMicrotask)。规则变成了:调用栈每走空一次,先一口气清空整个微任务队列,然后才取一个宏任务,取完再清微任务——微任务天然插队。

这就是“为什么 Promise.then 总是排在 setTimeout(fn,0) 前面”这道烂大街面试题的标准答案。async/await 只是 Promise 的语法糖,await 后面的代码相当于塞进微任务队列,所以它也遵守同一套规则。平心而论,这部分内容不影响对演讲核心模型的理解,但不了解它,你照着2014年的模型去预测今天的代码顺序,会系统性翻车——这也提醒我们:经典教程要读,但要知道它写于哪一年。

7. 这个模型为什么赢了全世界(延伸分析)

最后跳出来看大局。事件循环不是 JavaScript 的发明,它源自操作系统时代就有的 I/O 多路复用思想(select/poll 那一族),但确实是 JS 和浏览器把它变成了地球上最多人使用的并发模型。2009年 Node.js 用它把同一套模型搬进服务端;再往前看,Nginx 用事件驱动以极少的工作线程扛住海量连接,Redis 的命令执行至今基本在单线程内完成,依然快得惊人——它们的共同哲学是:慢的不是干活,是等待;把等待统一交给内核去排队,干活的人只管干活。

需要提醒一句:很多人把结论误读成“单线程所以快”。不对——单线程不代表快,代表简单和可预测:不用加锁、没有数据交错、UI 状态天然一致。真正的速度来自异步 I/O 把等待时间外包了出去。代价也很明确:一旦遇到计算密集型任务(大文件哈希、复杂运算),单线程立刻成为瓶颈,这时候才轮到 Web Worker 出场——Worker 里没有 DOM,但有自己的事件循环,通过消息传递和主线程协作。至此你也能看清它与《并发不是并行》那期经典演讲的关系:那是“多核时代如何利用多个工人”,这是“单工人如何永不空等”,两块拼图合起来才是完整的并发世界观。

知识结构图

flowchart TD
    Code["一段JS代码"] --> Sync["同步语句"]
    Code --> Async["异步调用"]
    Sync --> Stack["调用栈"]
    Async --> API["Web API接管<br/>定时器、请求、DOM事件"]
    API --> Macro["宏任务队列<br/>setTimeout与事件回调"]
    API --> Micro["微任务队列<br/>Promise与await"]
    Micro --> First["栈空后先清空微任务"]
    Macro --> Wait["排队等待"]
    Stack --> Empty["调用栈被清空"]
    Empty --> First
    First --> Next["只取一个宏任务"]
    Next --> Stack
    Loop["事件循环<br/>守门调度员"] -.->|盯着| Empty
    Render["页面渲染"] -.->|栈空闲时见缝插针| Empty

实战启示

第一,重排你的执行顺序直觉。 以后写 console.log 排序题,别再背口诀,直接推演三个盒子:同步的立即执行;Promise/await 进微任务,栈空后立即全部清空;setTimeout/事件回调进宏任务,等微任务清完才取一个。所有“玄学顺序”都是这条规则的推论,包括为什么 await 后面的代码比同一轮的 setTimeout 先跑。

第二,把长任务切碎,救回你的 INP 分数。 一个50毫秒以上的长任务就足以让点击响应变得可感知地迟钝。做法是把大计算切片:每处理一批数据,await 一个空定时器或用新的 scheduler.yield() 让出主线程,给渲染和输入事件留一条缝。列表渲染分帧、大数组分批处理,都是这个思路的落地。

第三,别再用同步的方式写“等待”。 同步 AJAX 早已是历史垃圾堆里的东西,但它的现代变种还在:在主线程同步解析超大 JSON、同步 alert 打断用户、把复杂校验塞进输入框的每次 keypress。判断标准只有一条:这段代码会不会让调用栈被占用超过一眨眼? 会,就异步化、切片化,或者丢进 Web Worker。

第四,调试异步 bug 时先画盒子。 遇到“回调执行了但顺序不对”“状态更新慢一拍”,先把代码按三个盒子归类,再按“同步→微任务→宏任务→渲染”的顺序手动走一遍。也可以把代码粘进 Loupe 亲眼看一遍搬家过程——花十分钟,胜过读十篇博客。

延伸学习路径

  1. Loupe 可视化工具——演讲同款,把事件循环跑给你看,支持自己写代码观察;
  2. MDN 官方文档:JavaScript 事件循环——含微任务语义的权威说明,补齐2014年之后的规则变化;
  3. Jake Archibald《Tasks, microtasks, queues and schedules》——演讲之后最重要的补课文章,用五个实验把两类队列的边界钉死;
  4. Node.js 官方文档:事件循环、定时器与 process.nextTick——看懂服务端版事件循环的分阶段模型,理解 setImmediate 与 setTimeout 的江湖恩怨;
  5. 顺着本文第7节,去补一场《并发不是并行》类的多核并发演讲,把“单工人不空等”和“多工人分工”两套模型合龙。

一句话总结

它用26分钟和几个会飞的小方块,把“单线程的 JavaScript 为什么不卡”钉死成一张图——读懂这张图,你对异步的一切困惑都有了坐标。

参考来源

  1. What the heck is the event loop anyway? — Philip Roberts — JSConf EU 2014,本文分析的核心视频
  2. Loupe:事件循环可视化工具 — 演讲者开源的配套演示工具
  3. MDN:JavaScript 事件循环 — 异步模型与微任务的官方权威说明
  4. Jake Archibald:Tasks, microtasks, queues and schedules — 微任务排队规则的最佳补课材料
  5. Node.js:事件循环、定时器与 process.nextTick — 服务端版事件循环官方文档

本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入