Skip to content

浏览器事件循环(Event Loop)

事件循环(Event Loop)又叫做消息循环(Message Loop),是浏览器渲染主线程的工作方式。它通过调度不同优先级的任务,保证了 JavaScript 单线程环境下 UI 交互与逻辑执行的平衡。

现代浏览器(如 Chrome/Edge)已不再使用单一的“宏任务队列”。取而代之的是多任务队列机制,根据任务来源分类并动态调度。

运行原理

渲染主线程与调用栈

渲染主线程是浏览器最繁忙的线程,负责解析 HTML/CSS、计算布局、执行 JS 和渲染页面。调用栈(Call Stack)则负责管理同步代码的执行。

多队列模型 (Task Queues)

根据 W3C 规范,同类型的任务必须在同一个队列,不同类型的任务可以分属不同队列。浏览器根据任务的急迫程度动态决定取哪个队列的任务:

  • 微任务队列 (Microtask Queue):优先级最高,必须最先清空。
  • 交互队列 (Interaction Queue):存放用户操作(点击、滚动、输入等)的回调。为保证响应速度,优先级极高。
  • 延时队列 (Timer Queue):存放 setTimeoutsetInterval 的回调。
  • 网络队列 (Network Queue):存放 Ajax/Fetch 网络请求的回调。

事件循环流转逻辑

  1. 执行任务 (Task):从某个任务队列中取出一个高优先级的任务放入调用栈执行(初次执行时是整个 script 脚本)。
  2. 清空微任务 (Microtasks)
    • 当一个任务执行完毕,主线程会立即检查微任务队列
    • 必须清空微任务队列中的所有任务,包括在执行微任务过程中产生的新的微任务。
  3. 渲染检查 (Rendering)
    • 微任务清空后,浏览器检查是否需要更新 UI。
    • 若达到渲染时机(通常 16.7ms),依次执行:requestAnimationFrame -> Layout -> Paint
  4. 循环:进入下一次迭代,从任务队列中挑选下一个任务执行。

🤔 任务 (Task) 与渲染

  • 微任务会阻塞渲染:微任务必须在渲染前全部执行完。如果你在微任务里递归产生微任务,浏览器将永远无法进入渲染阶段,导致页面卡死。
  • 任务间隙是渲染窗口:浏览器执行不同队列的任务是逐个进行的。每执行完一个 Task,浏览器都有权根据当前帧率决定是否插入渲染。

🤔 requestAnimationFrame (rAF) 的位置

rAF 既不属于微任务,也不属于常规任务队列。它是渲染流水线中的一个钩子函数,发生在微任务清空之后、显示器刷新画面之前。

🤔 为什么计时器(Timer)不精确?

  • 队列等待:即使时间到了,如果主线程正在执行一个耗时的 Task 或大量的微任务,计时器的回调也只能排队。
  • 嵌套限制:HTML 规范规定,setTimeout 嵌套层级超过 5 层时,最小间隔被强制设为 4ms。
  • 系统精度:受限于操作系统内核的调度精度。

🤔 异步

  • 单线程是异步产生的原因
  • 事件循环是异步的实现方式

🤔 如何理解JavaScript中的异步?

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面、执行JS都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。