首页 › programming › 事件循环神课:JS为何一心一用

事件循环神课:JS为何一心一用——一条百万播放的编程神课深度解读

概述

在编程教学视频的浩瀚海洋里,有一条视频的地位近乎“教义”级别:Philip Roberts 在 JSConf EU 2014 上的演讲《What the heck is the event loop anyway?》(事件循环到底是个啥?)。十多年过去,它仍是无数开发者理解 JavaScript 异步机制的“开窍第一课”,播放量累计达数百万量级,评论区常年有程序员留言“这是我终于搞懂异步的那天”。这篇文章把这条视频的完整知识脉络拆开给你:单线程为什么不可怕、阻塞到底卡在哪、setTimeout 为什么不守时、以及2014年那个“单队列模型”在今天需要补充什么。

视频基本信息

项目 内容
视频标题 What the heck is the event loop anyway?
中文译名 事件循环到底是个啥?
演讲者 Philip Roberts(据视频内容,其当时是一位 JavaScript 开发者,也是可视化工具 Loupe 的作者)
频道 JSConf(欧洲场次官方频道)
发布时间 2014年9月于柏林 JSConf EU 现场演讲,同年由官方频道上传
热度 据YouTube公开页面,播放量累计达数百万量级;本文化成时未能实时抓取页面,精确数字请以频道页面为准

数据说明:本文写作时网络抓取通道不可用,上述热度数据基于该视频长期以来的公开页面信息给出,未做实时核验;文中链接均为长期稳定的公开地址,访问异常时请以搜索标题为准。

这条视频值得精读的理由很朴素:它用不到30分钟,把一个被无数博客讲糊涂了的概念讲成了“一眼看穿”的白板图。更妙的是,它逼你回答一个日常会踩坑的问题——一段 JavaScript 代码从“发起”到“执行”,中间到底经过了谁的手?

核心主张拆解

按照演讲的叙事顺序,Roberts 的论证是一个漂亮的“起承转合”结构:先立靶子,再拆机器,最后给规则。

一、单线程的真相:一个堆、一个栈

演讲开篇,Roberts 给出那句被引用了十年的开场定义(大意):JavaScript 是一门单线程语言,它有一个调用栈(call stack)、一个堆(heap),就这些了。

堆负责内存分配,和今天的故事关系不大;真正的主角是调用栈——记录“当前执行到哪个函数”的数据结构。函数调用就入栈,返回就出栈,栈顶永远代表“正在执行的那一步”。这个模型简单到近乎简陋,但 Roberts 紧接着点出它的真正含义:单线程 = 同一时刻只能做一件事,且做这件事时不被打断。

为什么这很重要?因为在浏览器里,主线程干的活可不止跑你的 JS——渲染页面、响应用户点击,都在同一条线程上。这是整场演讲埋下的最大伏笔。

二、阻塞:慢的从来不是语言,是“等”

接下来 Roberts 现场演示了那个经典的反面教材:alert('hello')。弹窗不点确定,页面完全冻结——滚动不了、点击无效。为什么?因为 alert 是同步的:调用栈里压着它,栈非空,浏览器什么都干不了,只能干等。

他由此给出“什么算慢代码”的新视角:“慢”的本质不是 CPU 算不动,而是调用栈被长期占住。 网络请求慢、定时器没到点、用户输入迟迟不来——这些“等”如果发生在调用栈里,整个页面就陪着你一起罚站。这就是“阻塞(blocking)”一词在浏览器语境下的精确含义。

这一段的价值在于把一个模糊的体感词(“卡”)翻译成了精确的机制描述(“栈非空的时间太长”)。后文的批判性审查部分你会看到,这个标准在今天的 Web 性能指标里已经被量化成了具体毫秒数。

三、引擎之外:setTimeout 根本不在 JavaScript 里

这是全片最“值钱”的一个知识点,也是最多人第一次听说的转折。

Roberts 指出:大家熟悉的 setTimeout、DOM 事件、AJAX 请求——这些都不是 JavaScript 引擎(如 V8)提供的。V8 的世界里只有调用栈和堆。定时器、网络、事件监听,全部由宿主环境(浏览器、Node.js)提供,统称 Web API。它们在引擎之外并行存在:你调用 setTimeout 时,浏览器接住这个回调,自己在引擎外倒数计时,时间到了再把回调丢进一个队列。

证据摆得非常直观:如果你在只含引擎的环境里调用 setTimeout,它压根不存在。这个区分一举解释了大量疑惑——为什么 JS 是单线程却“能同时”处理点击和请求?答案是:异步能力根本不在 JS 语言里,而在环境给它的“外包团队”手里。

四、排队规则:调用栈清空才轮到你

机器拆完了,Roberts 给出那条唯一的运行规则,也就是事件循环(event loop)本身:

事件循环的职责简单到一句话:盯着调用栈,栈一空,就从回调队列(callback queue)里取出第一个回调,压进调用栈。就这么循环往复。

如果栈非空,队列里积压再多回调也得等;如果栈和队列都空,主线程就去渲染页面或休眠。Roberts 还强调了一个反直觉的点(大意):事件循环之所以很少被正经开讲,恰恰因为它简单得不像个“机制”——但正是这个极简机制,撑起了整个 JavaScript 世界的并发幻觉。

他用自己写的可视化工具 Loupe 现场跑了几段代码:console.log 同步语句、setTimeout 回调、模拟的 AJAX——观众肉眼看到回调们如何在栈空之后按入队顺序逐个被执行。这条视频最出圈的截图,就来自这几分钟。

五、setTimeout(fn, 0) 不是“立刻”

收尾部分 Roberts 拆解了那个流传极广的面试题:setTimeout(fn, 0) 是不是“零延迟立即执行”?答案是否定的。0 的真实语义是“最快也要等当前栈清空、且轮到你出队时才执行”——它表达的不是时刻,而是排队位置。哪怕写 0 毫秒,回调也得老老实实进队,等主线程忙完手头的一切。这个细节直接决定了大量“为什么我的代码执行顺序和写的不一样”问题的答案。

知识与理论溯源

这条演讲看似轻松,背后的概念每一个都有真实出处。把这条视频吃透,等于打通了半部异步编程的思想史。

事件驱动与 Reactor 模式

事件循环并非 JavaScript 发明。它的学术源头之一是 Douglas C. Schmidt 1995 年总结的 Reactor(反应器)模式——用“一个线程等待事件、事件到达后分发给处理器”的结构,让单线程高效服务大量并发事件。Node.js 作者 Ryan Dahl 在 2009 年首次公开介绍 Node 的演讲中,明确把 Reactor 模式列为设计基石;Node 底层的 libuv 库正是这一模式的工业级实现。浏览器的事件循环与 Node 的循环同宗同源,只是宿主不同。

协作式调度:一本操作系统教科书的视角

“调用栈非空时谁也插不进手”,在操作系统理论里叫协作式多任务(cooperative multitasking):任务不主动让出控制权,调度者就无能为力。这与操作系统主流的抢占式调度形成对照。经典教材《Operating Systems: Three Easy Pieces》(Arpaci-Dusseau 夫妇著)专门讨论过协作式调度的致命弱点——一个死循环就能拖垮整个系统。alert 冻结页面,正是教科书理论在浏览器里的现场重演。

与另外两条并发路线对照

理解事件循环的最好方式之一,是看它“不是什么”:

JavaScript 选了一条最“省”的路:不搞多线程通信,干脆一个线程 + 排队。代价是算力上限,收益是你几乎永远不用写锁。

规范层面的细节

十年后的现代回响

Roberts 的“别堵主线程”在今天已经是可量化的工程指标:Google web.dev 体系将超过 50 毫秒的主线程任务定义为长任务(Long Task),并以 INP(Interaction to Next Paint,交互到下一帧绘制)等指标衡量卡顿;Chrome 还陆续落地了 scheduler.yield() 等 API,让长任务可以主动“让出”主线程再续着跑——本质上是给协作式调度补了一副刹车。2018 年 Jake Archibald 的演讲《In The Loop》(JSConf Asia)被视为这条视频的官方续集,把微任务规则讲到了当前规范的精度。

批判性审查

证据充分的部分:单线程、调用栈、Web API 与引擎分离、回调队列、事件循环规则——这些内容与 HTML 规范、ECMAScript 规范及 V8/libuv 的公开文档完全一致,属于教科书级正确。Loupe 的可视化演示也与真实执行顺序吻合(工具本身开源,至今可用)。

被简化或过时的部分,有三处需要打补丁:

  1. “一条队列”是过时的。 现代执行模型至少有宏任务、微任务两条队列,微任务优先清空。只记单队列模型,遇到“Promise 和 setTimeout 谁先执行”这类问题必然答错——正确答案是 Promise 的微任务先。
  2. “JS 永远单线程”是误读。 准确说法是“主线程只有一个”。Web Worker 起的是真线程,各跑各的事件循环,靠消息传递通信;ES2017 之后 SharedArrayBuffer 配合 Atomics 甚至支持共享内存。单线程是默认约定,不是物理牢笼。
  3. 浏览器与 Node 不能一概而论。 Node 的事件循环分阶段(timers、poll、check 等),setImmediate 与 setTimeout(0) 的先后、process.nextTick 的插队规则都是 Node 特有,直接套用演讲里的浏览器模型会翻车。

一个可以继续深挖的争议点:有人批评这类演讲让初学者把“异步”等同于“回调排队”,忽视了取消、竞态、错误传播这些异步编程真正的难点。这个批评有道理——但把它当作“入门第一图”而非“终局答案”,视频本身没有任何问题。

认知提升与实用启示

1. 给自己的异步代码拍一张“X光片”。 打开 Loupe(latentflip.com/loupe),把一段拿不准执行顺序的代码粘进去,亲眼看它入栈、进队、出栈。十分钟的可视化抵得上十篇博客。进阶用 Chrome DevTools 的 Performance 面板录一段真实页面,找出超过 50 毫秒的长任务——它们就是“卡”的元凶,能拆则拆(拆分循环、用 scheduler.yield() 让出主线程)。

2. 建立“栈—队列—循环”三格心算图。 以后看到任何异步代码,先默画三格:当前栈里有什么、微任务队列有什么、宏任务队列有什么,然后按“栈清空→清空微任务→取一个宏任务”的口令推演。90% 的“执行顺序玄学”会当场现形,这也是异步面试题的标准解法。

3. 把“是否阻塞”当作架构选型的第一问。 选型时先分清任务类型:I/O 密集(等网络、等磁盘)→ 单线程事件循环是天堂,Node 的存在就是证明;计算密集(大数组运算、加密、图像处理)→ 别在主线程硬扛,交给 Web Worker 或干脆换多线程方案。理解能力边界,比记住 API 重要得多。

视频论证结构图

flowchart TD
    A["JS单线程<br/>一个堆一个栈"] --> B["同步代码阻塞<br/>alert冻结页面"]
    B --> C["转折:定时器网络事件<br/>都不在JS引擎里"]
    C --> D["宿主环境的Web API<br/>在引擎外接活"]
    D --> E["回调完成入队<br/>回调队列排队"]
    E --> F{"事件循环监工<br/>调用栈空了吗?"}
    F -->|非空| G["继续等待<br/>谁都别想插队"]
    G --> F
    F -->|为空| H["取队首回调<br/>压入调用栈执行"]
    H --> I["setTimeout零毫秒<br/>也要排队不插队"]
    I --> F

延伸阅读

  1. Jake Archibald《In The Loop》(JSConf Asia 2018)——本片的官方续集,把微任务、渲染时机、rAF 的顺序讲到了当前规范精度,看懂前作后必看。
  2. Loupe 可视化工具(latentflip.com/loupe)——视频里那个演示工具本身,把任意异步代码变成动画白板。

参考来源

  1. What the heck is the event loop anyway? - Philip Roberts, JSConf EU — 本文解读的主体视频
  2. Loupe — Philip Roberts 的异步可视化教学工具 — 视频演示所用工具,可在线体验
  3. In The Loop - Jake Archibald, JSConf Asia 2018 — 微任务与渲染时机的权威续讲
  4. Reactor pattern - Wikipedia — 事件循环的学术源头:Schmidt 1995 年反应器模式
  5. Node.js 官方文档:事件循环、定时器与 process.nextTick — Node 侧事件循环分阶段机制的官方说明

本文由「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学通信知识星球海报-扫码加入