浏览器下的 Event Loop

60 阅读3分钟

单线程的JavaScript

在前端开发中,我们经常听到“JavaScript 是单线程的,但能实现异步非阻塞”。这个“魔法”背后的核心机制就是 Event Loop(事件循环)。很多人将 Event Loop 单纯归为 JavaScript 的特性,但实际上,它更准确地说是浏览器环境提供的运行时机制。

多线程的浏览器架构

很多人误以为浏览器是单线程的,其实现代浏览器(如 Chrome、Edge、Firefox)采用多进程 + 多线程架构,确保页面流畅、网络高效、渲染顺滑。

以 Chrome 为例,主要线程包括:

  • 主线程(Main Thread):执行 JS、解析 HTML/CSS、布局计算(Layout)、样式重算(Style)。
  • 合成器线程(Compositor Thread):处理滚动、transform/opacity 动画,不阻塞主线程。
  • 光栅化线程(Raster Thread):将绘制指令转为位图,交给 GPU。
  • 网络线程、IO 线程、Worker 线程 等:处理请求、磁盘操作、Web Workers。

JS “单线程”仅指它在渲染进程的主线程上运行一个调用栈,避免多线程同步问题。但异步操作(如 setTimeout、Ajax)会交给浏览器的 Web APIs(运行在其他线程),完成后回调再排队等待。

Event Loop 图解

13b55c93-325d-4c1f-ab3c-cfe20e5be232.png

  • Heap:对象内存分配。
  • Stack:调用栈,函数执行上下文。
  • Web APIs:浏览器提供的异步接口(DOM、Ajax、setTimeout 等)。
  • Callback Queue:回调函数排队等待。
  • Event Loop:检查栈空闲时,从队列取回调执行。

这个模型直观解释了异步“假象”:耗时任务外包给浏览器线程,回调再进入队列。

但它有个局限——只描述了宏任务队列,无法解释 Promise 等微任务的优先执行。

微任务队列 vs 宏任务队列

Event Loop 其实维护两个队列

  • 队列

67c3765230c7d1a76b07873de5f1c1e3.png

  • 执行

1_XVqPA2z1dTHJWm2TwIAsBw.gif

  • 宏任务(Macrotask):setTimeout、setInterval、I/O、UI 渲染等。队列可能很长,每次循环只执行一个。
  • 微任务(Microtask):Promise.then、MutationObserver、queueMicrotask 等。队列通常较短,一个宏任务结束后,必须一次性清空所有微任务(包括执行中新产生的)。

精确执行流程(每帧 ~16ms):

  1. 从宏任务队列取一个任务执行(初始为整个 script)。
  2. 执行中产生的宏任务入宏队列,微任务入微队列。
  3. 当前宏任务结束(栈清空)。
  4. 执行全部微任务,直到队列为空。
  5. 浏览器进行渲染更新(Style → Layout → Paint → Composite)。
  6. 进入下一轮,执行下一个宏任务。

这种设计确保微任务高优先级响应,同时为渲染留出时机,避免无限微任务链完全阻塞页面。

代码示例

console.log('1. start');

setTimeout(() => console.log('2. setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('3. Promise1'))
  .then(() => console.log('4. Promise2'));

queueMicrotask(() => console.log('5. queueMicrotask'));

console.log('6. end');

输出:

1. script start
6. script end
3. Promise1
5. queueMicrotask
4. Promise2
2. setTimeout

同步代码先执行,微任务在宏任务结束后批量清空,setTimeout 推迟到下一轮。

浏览器 Event Loop 是多线程协作与单线程 JS 的完美平衡。从简化模型到双队列机制,它体现了性能与响应性的极致权衡。