事件循环
事件循环不是抽象概念,它就是浏览器主线程调度任务的工作方式。
只有把进程、线程、消息队列和异步任务放在同一张图里看,才会真正理解为什么 JavaScript 看起来是单线程,却能持续处理复杂交互。
事件循环
浏览器的进程模型
1. 进程
进程是操作系统资源分配的最小单元。
程序运行需要有它的专属内存空间,可以把这个空间简单理解为一个进程。

每个应用至少有一个进程。进程之间相互独立,即使要通信,也需要双方同意。
一个进程可以包含多个线程。
2. 线程
线程是操作系统能够进行运算调度的最小单元,是进程中实际运行的单位。

有了进程之后,要真正运行代码,还需要“执行者”,这就是线程。
一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,这个线程被称为主线程。如果程序需要同时执行多块代码,主线程就会启动更多线程来处理,所以一个进程中可以包含多个线程。
主线程结束后,一般整个程序也就结束了。
3. 浏览器中的进程和线程
浏览器是一个多进程、多线程的应用程序,内部工作复杂,某种程度上可以类比一个小型操作系统。
为了避免互相影响,也为了减少连环崩溃的概率,浏览器启动后通常会拉起多个进程。

浏览器中通常可以看到三类关键进程:
- 浏览器进程:负责界面显示、用户交互、子进程管理等。浏览器进程内部也会启动多个线程处理不同任务。
- 网络进程:负责加载网络资源。网络进程内部同样会启动多个线程处理不同的网络任务。
- 渲染进程:渲染进程启动后,会开启一个渲染主线程。主线程负责执行 HTML、CSS、JavaScript 等代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证标签页之间互不影响。
渲染主线程是浏览器中最繁忙的线程之一,它需要持续处理很多事情:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 按帧绘制页面
- 执行全局 JavaScript
- 执行事件处理函数
- 执行计时器回调
思考:为什么渲染进程不使用多个线程分别处理这些事情?
当要处理的任务越来越多时,渲染主线程面临的核心问题其实是:如何调度任务。

它大致会按下面的方式工作:
- 渲染主线程进入一个不会结束的循环,可以简单理解为
for(;;;)。 - 每次循环都会检查消息队列里是否存在任务。
- 如果有任务,就取出第一个任务执行;执行完后进入下一轮循环。
- 如果没有任务,主线程就进入休眠状态。
- 其他线程或进程可以在任意时刻往消息队列中追加任务。
- 如果主线程此时处于休眠状态,新任务到来时会把它唤醒。
这样一来,每个任务就可以有条不紊地持续推进。这个过程,就叫作事件循环。
异步为什么存在
代码在执行过程中,经常会遇到一些无法立刻完成的任务,比如:
- 计时结束后执行的任务,例如
setTimeout、setInterval - 网络通信完成后执行的任务,例如
XHR、fetch - 用户操作后执行的任务,例如
addEventListener
如果让渲染主线程一直等待这些任务就绪,它就会长时间阻塞,页面也会因此卡住。
但渲染主线程承担着极其重要的工作,不能被长时间阻塞。所以浏览器会采用异步方式:主线程把这些等待型任务交给其他线程或系统能力去处理,自己继续往下执行别的任务,等时机成熟后,再把对应回调包装成新的任务,放回消息队列,等待主线程调度。
消息队列
任务本身在队列中通常是先进先出,但不同类型的任务会被放进不同的队列,而这些队列之间存在优先级差异。
根据 W3C 的解释:
- 每个任务都有自己的任务类型。
- 同一类型的任务必须进入同一个队列。
- 不同类型的任务可以位于不同队列。
- 浏览器会在一次事件循环中根据实际情况从不同队列中取任务执行。
- 浏览器必须准备一个微任务队列,并优先执行其中的任务。
随着浏览器复杂度不断提升,“宏任务队列 / 微任务队列” 这种过于简化的说法已经不足以覆盖真实实现。
在 Chrome 的实现里,至少可以抽象出下面几类典型队列:
- 延时队列:存放计时器到点后的回调任务,优先级中等
- 交互队列:存放用户交互产生的事件处理任务,优先级较高
- 微任务队列:存放需要尽快执行的任务,优先级最高
常见会进入微任务队列的有:
PromiseMutationObserver
常见思考
1. 如何理解 JavaScript 的异步
JavaScript 是单线程语言,因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
渲染主线程承担着渲染页面、执行 JavaScript、响应交互等大量工作。如果所有任务都同步阻塞执行,主线程就可能被长期占住,导致消息队列中的其他任务无法及时处理,页面也无法及时更新。
所以浏览器采用了异步机制来避免这种情况。对于计时器、网络请求、事件监听等任务,主线程先把工作交出去,自己继续执行后续代码;等这些任务准备好之后,再把回调加入消息队列,等待主线程调度执行。
这就是 JavaScript 异步背后的核心原因:避免阻塞主线程,尽可能保证单线程执行环境的流畅性。
2. 什么是事件循环
事件循环也叫消息循环,本质上是浏览器渲染主线程的工作方式。
在 Chrome 的实现中,可以把它理解成一个不会结束的循环。每次循环都会尝试从消息队列里取出一个任务来执行,而其他线程只需要在合适的时候把任务加入队列末尾即可。
过去大家习惯把它概括成“宏任务 + 微任务”,但在现代浏览器环境里,更准确的理解应该是:存在多个不同类型、不同优先级的任务队列;浏览器会根据规则选择执行;其中微任务队列拥有必须优先清空的特殊地位。
3. JavaScript 计时器能做到精确计时吗
不能。
- 操作系统层面的计时本身就可能有误差,而 JavaScript 计时器最终也是基于这些能力实现的。
- 计时器回调并不会在时间一到就立刻执行,它仍然要等待事件循环把它调度到主线程。
- 按照 W3C 标准,当计时器嵌套层级超过 5 层时,浏览器会施加最小时间限制,这也会带来额外偏差。
- 计算机本身并没有“绝对精确”的时间基准,所以计时器天然只能做到近似。
总结
单线程是异步产生的原因。
事件循环是异步的实现方式。