事件循环是前端开发中极其重要的基础内容,它是让 JavaScript 这种单线程语言能够执行非阻塞异步操作的秘密武器。
事件循环(Event Loop)是前端开发中极其重要的基础内容,作为前端开发者必须要掌握的内容。同时在日常的工作或者面试中也是高频的话题。
在讲事件循环之前,先了解一下计算机科学中的两个基本执行单元:进程和线程。
进程h2
进程是操作系统中资源分配的基本单位。它是程序的一次执行过程,拥有自己独立的内存空间、代码、数据等资源。一个进程可以包含多个线程,它们共享进程的资源,但是每个线程有自己的执行路径。在Windows系统中,一个运行的应用程序,如xx.exe,就是一个进程。
线程h2
线程是进程中的一个执行任务,是处理器任务调度和执行的基本单位。它是比进程更小的能独立运行的基本单位。线程共享其所属进程的地址空间和资源,但拥有自己的程序计数器、虚拟机栈和本地方法栈。线程之间的切换开销小于进程,因此被称为轻量级进程。

通过示意图,我们可以更好的理解他们的关系。每个应用程序,至少有一个进程,进程之间相互独立,如果要通信,也需要双方同意。每个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,称之为主线程。如果要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。
浏览器h2
浏览器是一个多进程多线程的应用程序。浏览器内部工作极其复杂,为了减少连环崩坏的几率,它会自动启动多个进程。
以 Chrome 为例,通过快捷键 shift + esc,可以快速打开浏览器的任务管理器,可以看见有多个进程在运行。

前端开发最关注的是渲染进程,每开启一个 Tab 页,就会有一个渲染进程。渲染进程下面一般是,主线程、合成线程、栅格线程、IO线程、Workder相关线程。 其中有一个误解就是会认为有一个独立的JS引擎线程,但实际上,JS引擎是运行在主线程中的,它与HTML/CSS解析、样式计算、布局、绘制串行执行,会与 GUI 渲染互斥,所以JS会阻塞渲染。 参考官方核心文档-Threading and Tasks in Chrome。
浏览器中的事件循环机制h2
事件循环是单线程 JS 支持异步的核心机制。当执行栈中的同步任务执行完毕并清空后,事件循环会按照特定规则,从任务队列中提取并执行异步任务的回调函数。本质上,它是“事件驱动”模式在 JS 运行环境中的具体调度实现。
事件驱动h3
浏览器异步任务的执行原理背后其实是一套事件驱动的机制。事件触发、任务选择和任务执行都是由事件驱动机制来完成的。NodeJS 和浏览器的设计都是基于事件驱动的,简而言之就是由特定的事件来触发特定的任务,这里的事件可以是用户的操作触发的,如 click 事件;也可以是程序自动触发的,比如浏览器中定时器线程在计时结束后会触发定时器事件。
在事件驱动中,当有事件触发后,被触发的事件(上面所说的回调函数)会按顺序暂时存在一个队列中,待 JS 的同步任务执行完成后,会从这个队列中取出要处理的事件并进行处理。那么具体什么时候取任务、优先取哪些任务,这就由事件循环流程来控制了。
执行栈与任务队列h3
JS 在解析一段代码时,会把同步代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务就会交给其他线程处理,待当前执行栈中的所有同步代码执行完成后,会从任务队列中去取出早已完成的异步任务的回调函数加入执行栈继续执行,遇到异步任务又交给其他线程处理,…如此循环往复。
JS 按顺序执行执行栈中的方法,每次执行一个方法时,会为这个方法生成独有的执行环境(上下文 context),待这个方法执行完成后,销毁当前的执行环境,并从栈中弹出此方法(即消费完成),然后继续下一个方法。

可见,在事件驱动的模式下,至少包含了一个执行循环来检测任务队列是否有新的任务。通过不断循环去取出异步回调来执行,这个过程就是事件循环,而每一次循环就是一个事件周期或称为一次 tick。
任务分类h3
任务没有优先级,但是消息队列有优先级,消息队列不止一个。
每一个任务都有任务类型,同一个类型的任务必须在同一个队列。浏览器必须准备一个微队列,微队列的优先级优先所有其他队列。
在以前,只有宏任务队列和微任务队列。但是随着浏览器的复杂度急剧发展,W3C不再使用宏队列的说法。
在目前的 chrome的实现中,至少包含了下面的队列:
- 延时队列:用于存放定时器到达后的回调函数,优先级「中」
- 交互队列:用于存放用户操作后的事件处理任务,优先级「高」
- 微队列:用于存在需要最快执行的任务,优先级「最高」
那么有什么办法可以添加任务到微队列呢?
- Promise:当 Promise 决议后,其
.then、.catch、.finally的回调会进入微队列。 - queueMicrotask:提供了专门用于添加微任务的原生 API(推荐)。
- MutationObserver:浏览器中用于监听 DOM 变化触发的回调,也属于微任务。
- process.nextTick(Node.js):虽不是标准微任务,但执行时机甚至比微任务更早。
定时器误差h2
事件循环中,总是先执行同步代码之后,才会去任务队列中取任务执行。那么如果同步代码执行时间过长,就会导致定时器不准时执行。 同步代码耗时越长,定时器误差就越大。不仅仅是同步代码,由于微任务的优先级最高,所以微任务也会影响计时,假设同步代码中有一个死循环或者微任务中递归不断在启动其他微任务,那么宏任务里面的代码可能永远得不到执行。所以主线程代码的执行效率提升是一件很重要的事情。
总结h2
通篇来看,我们可以得出以下核心结论:
- 单线程是基石:JavaScript 是一门单线程语言,其引擎运行在浏览器的渲染进程主线程中,执行过程与 GUI 渲染互斥。
- 事件驱动是模式:浏览器通过事件驱动机制管理异步任务,事件触发后产生回调任务,并按类型进入不同的任务队列(如微队列、延时队列、交互队列)。
- 事件循环是核心:它是连接“同步执行栈”与“异步任务队列”的调度官。基本逻辑是:执行同步代码(清空栈) -> 执行并清空所有微任务 -> 必要时进行 UI 渲染 -> 提取下一个异步回调,如此往复。
- 性能导向:理解事件循环不仅是为了应付面试,更是为了在开发中意识到主线程的宝贵。避免长时间同步阻塞和微任务堆积,是确保定时器精准、页面丝滑响应的关键。
掌握了事件循环,就掌握了 JavaScript 在浏览器中运行的“心跳”。
评论