返回博客列表
Essay 05 · JavaScript

事件循环

事件循环不是抽象概念,它就是浏览器主线程调度任务的工作方式。

只有把进程、线程、消息队列和异步任务放在同一张图里看,才会真正理解为什么 JavaScript 看起来是单线程,却能持续处理复杂交互。

2026年4月4日9 min read
JavaScriptEvent LoopBrowser

事件循环

浏览器的进程模型

1. 进程

进程是操作系统资源分配的最小单元。

程序运行需要有它的专属内存空间,可以把这个空间简单理解为一个进程。

截屏2026-04-04 20.12.38

每个应用至少有一个进程。进程之间相互独立,即使要通信,也需要双方同意。

一个进程可以包含多个线程。

2. 线程

线程是操作系统能够进行运算调度的最小单元,是进程中实际运行的单位。

截屏2026-04-04 20.16.33

有了进程之后,要真正运行代码,还需要“执行者”,这就是线程。

一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,这个线程被称为主线程。如果程序需要同时执行多块代码,主线程就会启动更多线程来处理,所以一个进程中可以包含多个线程。

主线程结束后,一般整个程序也就结束了。

3. 浏览器中的进程和线程

浏览器是一个多进程、多线程的应用程序,内部工作复杂,某种程度上可以类比一个小型操作系统。

为了避免互相影响,也为了减少连环崩溃的概率,浏览器启动后通常会拉起多个进程。

截屏2026-04-04 20.21.39

浏览器中通常可以看到三类关键进程:

  • 浏览器进程:负责界面显示、用户交互、子进程管理等。浏览器进程内部也会启动多个线程处理不同任务。
  • 网络进程:负责加载网络资源。网络进程内部同样会启动多个线程处理不同的网络任务。
  • 渲染进程:渲染进程启动后,会开启一个渲染主线程。主线程负责执行 HTML、CSS、JavaScript 等代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证标签页之间互不影响。

Chromium

渲染主线程是浏览器中最繁忙的线程之一,它需要持续处理很多事情:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 按帧绘制页面
  • 执行全局 JavaScript
  • 执行事件处理函数
  • 执行计时器回调

思考:为什么渲染进程不使用多个线程分别处理这些事情?

当要处理的任务越来越多时,渲染主线程面临的核心问题其实是:如何调度任务。

截屏2026-04-04 20.33.50

它大致会按下面的方式工作:

  1. 渲染主线程进入一个不会结束的循环,可以简单理解为 for(;;;)
  2. 每次循环都会检查消息队列里是否存在任务。
  3. 如果有任务,就取出第一个任务执行;执行完后进入下一轮循环。
  4. 如果没有任务,主线程就进入休眠状态。
  5. 其他线程或进程可以在任意时刻往消息队列中追加任务。
  6. 如果主线程此时处于休眠状态,新任务到来时会把它唤醒。

这样一来,每个任务就可以有条不紊地持续推进。这个过程,就叫作事件循环

异步为什么存在

代码在执行过程中,经常会遇到一些无法立刻完成的任务,比如:

  • 计时结束后执行的任务,例如 setTimeoutsetInterval
  • 网络通信完成后执行的任务,例如 XHRfetch
  • 用户操作后执行的任务,例如 addEventListener

如果让渲染主线程一直等待这些任务就绪,它就会长时间阻塞,页面也会因此卡住。

但渲染主线程承担着极其重要的工作,不能被长时间阻塞。所以浏览器会采用异步方式:主线程把这些等待型任务交给其他线程或系统能力去处理,自己继续往下执行别的任务,等时机成熟后,再把对应回调包装成新的任务,放回消息队列,等待主线程调度。

消息队列

任务本身在队列中通常是先进先出,但不同类型的任务会被放进不同的队列,而这些队列之间存在优先级差异。

根据 W3C 的解释:

  • 每个任务都有自己的任务类型。
  • 同一类型的任务必须进入同一个队列。
  • 不同类型的任务可以位于不同队列。
  • 浏览器会在一次事件循环中根据实际情况从不同队列中取任务执行。
  • 浏览器必须准备一个微任务队列,并优先执行其中的任务。

随着浏览器复杂度不断提升,“宏任务队列 / 微任务队列” 这种过于简化的说法已经不足以覆盖真实实现。

在 Chrome 的实现里,至少可以抽象出下面几类典型队列:

  • 延时队列:存放计时器到点后的回调任务,优先级中等
  • 交互队列:存放用户交互产生的事件处理任务,优先级较高
  • 微任务队列:存放需要尽快执行的任务,优先级最高

常见会进入微任务队列的有:

  • Promise
  • MutationObserver

常见思考

1. 如何理解 JavaScript 的异步

JavaScript 是单线程语言,因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

渲染主线程承担着渲染页面、执行 JavaScript、响应交互等大量工作。如果所有任务都同步阻塞执行,主线程就可能被长期占住,导致消息队列中的其他任务无法及时处理,页面也无法及时更新。

所以浏览器采用了异步机制来避免这种情况。对于计时器、网络请求、事件监听等任务,主线程先把工作交出去,自己继续执行后续代码;等这些任务准备好之后,再把回调加入消息队列,等待主线程调度执行。

这就是 JavaScript 异步背后的核心原因:避免阻塞主线程,尽可能保证单线程执行环境的流畅性。

2. 什么是事件循环

事件循环也叫消息循环,本质上是浏览器渲染主线程的工作方式。

在 Chrome 的实现中,可以把它理解成一个不会结束的循环。每次循环都会尝试从消息队列里取出一个任务来执行,而其他线程只需要在合适的时候把任务加入队列末尾即可。

过去大家习惯把它概括成“宏任务 + 微任务”,但在现代浏览器环境里,更准确的理解应该是:存在多个不同类型、不同优先级的任务队列;浏览器会根据规则选择执行;其中微任务队列拥有必须优先清空的特殊地位。

3. JavaScript 计时器能做到精确计时吗

不能。

  1. 操作系统层面的计时本身就可能有误差,而 JavaScript 计时器最终也是基于这些能力实现的。
  2. 计时器回调并不会在时间一到就立刻执行,它仍然要等待事件循环把它调度到主线程。
  3. 按照 W3C 标准,当计时器嵌套层级超过 5 层时,浏览器会施加最小时间限制,这也会带来额外偏差。
  4. 计算机本身并没有“绝对精确”的时间基准,所以计时器天然只能做到近似。

总结

单线程是异步产生的原因。

事件循环是异步的实现方式。