浏览器事件循环(Event Loop)
事件循环(Event Loop)又叫做消息循环(Message Loop),是浏览器渲染主线程的工作方式。它通过调度不同优先级的任务,保证了 JavaScript 单线程环境下 UI 交互与逻辑执行的平衡。
现代浏览器(如 Chrome/Edge)已不再使用单一的“宏任务队列”。取而代之的是多任务队列机制,根据任务来源分类并动态调度。
运行原理
渲染主线程与调用栈
渲染主线程是浏览器最繁忙的线程,负责解析 HTML/CSS、计算布局、执行 JS 和渲染页面。调用栈(Call Stack)则负责管理同步代码的执行。
多队列模型 (Task Queues)
根据 W3C 规范,同类型的任务必须在同一个队列,不同类型的任务可以分属不同队列。浏览器根据任务的急迫程度动态决定取哪个队列的任务:
- 微任务队列 (Microtask Queue):优先级最高,必须最先清空。
- 交互队列 (Interaction Queue):存放用户操作(点击、滚动、输入等)的回调。为保证响应速度,优先级极高。
- 延时队列 (Timer Queue):存放
setTimeout、setInterval的回调。 - 网络队列 (Network Queue):存放 Ajax/Fetch 网络请求的回调。
事件循环流转逻辑
- 执行任务 (Task):从某个任务队列中取出一个高优先级的任务放入调用栈执行(初次执行时是整个
script脚本)。 - 清空微任务 (Microtasks):
- 当一个任务执行完毕,主线程会立即检查微任务队列。
- 必须清空微任务队列中的所有任务,包括在执行微任务过程中产生的新的微任务。
- 渲染检查 (Rendering):
- 微任务清空后,浏览器检查是否需要更新 UI。
- 若达到渲染时机(通常 16.7ms),依次执行:
requestAnimationFrame->Layout->Paint。
- 循环:进入下一次迭代,从任务队列中挑选下一个任务执行。
🤔 任务 (Task) 与渲染
- 微任务会阻塞渲染:微任务必须在渲染前全部执行完。如果你在微任务里递归产生微任务,浏览器将永远无法进入渲染阶段,导致页面卡死。
- 任务间隙是渲染窗口:浏览器执行不同队列的任务是逐个进行的。每执行完一个 Task,浏览器都有权根据当前帧率决定是否插入渲染。
🤔 requestAnimationFrame (rAF) 的位置
rAF 既不属于微任务,也不属于常规任务队列。它是渲染流水线中的一个钩子函数,发生在微任务清空之后、显示器刷新画面之前。
🤔 为什么计时器(Timer)不精确?
- 队列等待:即使时间到了,如果主线程正在执行一个耗时的 Task 或大量的微任务,计时器的回调也只能排队。
- 嵌套限制:HTML 规范规定,
setTimeout嵌套层级超过 5 层时,最小间隔被强制设为 4ms。 - 系统精度:受限于操作系统内核的调度精度。
🤔 异步
- 单线程是异步产生的原因
- 事件循环是异步的实现方式
🤔 如何理解JavaScript中的异步?
JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面、执行JS都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
@keyboarder-yang