
基于 WHATWG HTML Living Standard Chromium 最新源码架构2024–2026对浏览器事件循环机制进行完整梳理。一、为什么需要事件循环每个渲染进程都有一个主线程Main Thread它需要处理大量异构任务DOM 解析与构建样式计算Style Recalculation布局Layout / ReflowJavaScript 执行用户输入事件点击、滚动、键盘网络回调、定时器、动画……这些任务共享同一个主线程同一时刻只能执行一个。要让它们有条不紊地调度执行就需要一套统筹系统——消息队列 事件循环。关键认知JavaScript 引擎V8本身没有独立的循环系统。所谓JS Event Loop就是浏览器渲染进程提供的事件循环二者是同一套机制。二、线程模型的演进理解事件循环最好从最简单的场景逐步推演第一版顺序执行voidMainThread(){inta12;// 任务1intb20/5;// 任务2intc7*8;// 任务3print(a,b,c);// 任务4}所有任务按顺序写完执行完线程退出。无法响应运行时产生的新任务。第二版引入循环 事件voidMainThread(){for(;;){intxGetInput();// 阻塞等待用户输入intyGetInput();print(xy);}}改进引入for循环线程不再退出引入事件等待阻塞式 IO无事件时线程挂起有事件时激活局限只能处理线程内部产生的任务无法接收外部线程的消息。第三版引入消息队列跨线程通信TaskQueue task_queue;voidMainThread(){for(;;){Task tasktask_queue.takeTask();// 从头部取ProcessTask(task);}}// 其他线程投递任务voidIOThread(){task_queue.pushTask(someTask);// 添加到尾部}改进引入 FIFO 队列其他线程只需将任务推入队列多线程操作同一队列必须加同步锁第四版跨进程通信其他进程 ──IPC──→ 渲染进程 IO 线程 ──组装Task──→ 主线程消息队列其他进程Browser 进程、GPU 进程等通过Mojo IPC将消息发送给渲染进程的 IO 线程IO 线程将其组装为 Task 后投递到主线程的队列中。三、最新 Chromium 实现不再是一个队列3.1 旧说法 vs 新实现维度旧模型2019年前新模型当前 Chromium队列数量一个消息队列多个 TaskQueue按用途/优先级划分调度器简单 for 循环TaskSequenceManager 优先级调度算法术语“宏任务 / 微任务”规范只称task和microtask微任务归属“在宏任务内部的列表中”属于V8 引擎的 MicrotaskQueue不在浏览器 TaskQueue 中优先级未明确明确的多级优先级体系3.2 TaskSequenceManager 架构渲染进程主线程 └── TaskSequenceManager调度器 ├── TaskQueue: Control P0内部控制 ├── TaskQueue: UserInput P1用户交互 ├── TaskQueue: Default P2默认任务 ├── TaskQueue: Render P2渲染管线 ├── TaskQueue: Idle P3空闲任务 └── ...核心源码位置base/task/sequence_manager/sequence_manager_impl.h— 调度器base/task/sequence_manager/task_queue_impl.h— 单个队列third_party/blink/renderer/core/scheduler/— Blink 层注册v8/src/execution/microtask-queue.cc— 微任务队列3.3 关于宏任务术语WHATWG 规范中从未使用“macro task” 一词。规范原文“An event loop has one or moretask queues. Ataskis an atomic unit of work.”“Each event loop has amicrotask queue.”只有task和microtask两个层级。宏任务是社区为对比而创造的俗称严格场景应使用 “task”。3.4 循环不会 CPU 空转事件循环的for循环并非忙等待。底层采用系统级中断机制Linux 上为 epollWindows 上为 IOCP无消息时线程挂起休眠有消息时被操作系统唤醒不会消耗 CPU。四、Task 的优先级体系4.1 四级优先级优先级枚举值对应任务说明P0kControlPriority线程管理、生命周期信号用户不可见的内部任务P1kBestEffortPriorityclick、keydown、touchstart、scroll 回调用户交互最高响应优先级P2kUserVisiblePrioritysetTimeout/setInterval、fetch/XHR 回调、postMessage、DOM 解析、JS 脚本执行用户可见的默认任务P3kIdlePriorityrequestIdleCallback、低优先级后台任务仅在高优队列空闲时执行4.2 同优先级内的排序规则因素规则延迟任务setTimeout(fn, 100)到期前不参与选取到期后按 FIFO公平性调度器跟踪各队列累计执行时间防止高优队列饿死低优队列任务年龄等待过久的低优任务可能被临时提升嵌套深度深层嵌套 timer 可能被降权4.3 调度伪代码TaskSelectNextTask(){// 1. 按优先级从高到低扫描所有队列for(autoqueue:queues_sorted_by_priority){if(queue.has_ready_tasks()){returnqueue.take_front();}}// 2. 检查延迟任务是否到期// 3. 若无任务线程挂起等待系统事件}五、微任务Microtask独立的执行机制5.1 为什么微任务不在 Task Queue 中维度TaskMicrotask管理者浏览器框架TaskSequenceManagerV8 引擎v8::MicrotaskQueue存储位置TaskQueue浏览器层V8 Isolate 内部入队时机事件触发时当前 task 执行过程中产生执行时机由调度器按优先级选取当前 task 结束后立即同步清空能否被其他 task 插队能高优抢占低优不能同步执行不让出控制权5.2 微任务类型类型示例Promise 回调Promise.then / .catch / .finallyasync/await 续体await之后的代码编译为 Promise.thenMutationObserverDOM 变化监听回调queueMicrotask()显式入队微任务IntersectionObserver 回调部分实现中归入微任务5.3 执行规则Task A 执行中 → 产生 microtask M1 Task A 执行完毕 → 执行 M1 → M1 中产生 M2 → 执行 M2 → M2 中产生 M3 → 执行 M3 → 队列为空 ✅ → 才进入下一步渲染 or 下一个 Task递归清空微任务执行中产生的新微任务排到末尾继续执行直到队列彻底为空。⚠️陷阱递归 Promise 链会导致微任务无限增长阻塞后续所有 Task 和渲染更新造成页面卡死。5.4 微任务解决的原始问题DOM 变化监控面临两难同步通知每次 DOM 变化立即调用 JS → 拉长当前任务执行时间降低效率异步放入 Task Queue实时性差前面可能排了很多任务微任务的折中方案不打断当前 Task 执行解决效率问题当前 Task 结束后立即执行不等下一个 Task解决实时性问题六、哪些任务不经过 Task Queue6.1 分类总览渲染主线程的执行单元 │ ├── ✅ 经过 Task Queue 调度 │ ├── 用户输入事件click, keydown, scroll... │ ├── setTimeout / setInterval 回调 │ ├── I/O 回调fetch, FileReader, WebSocket... │ ├── postMessage 回调 │ ├── requestIdleCallback 回调 │ └── 外部进程/线程投递的任务 │ ├── ❌ 不经过 Task Queue │ │ │ ├── 微任务V8 MicrotaskQueue │ │ ├── Promise.then / catch / finally │ │ ├── MutationObserver │ │ ├── queueMicrotask() │ │ └── async/await 续体 │ │ │ ├── 渲染帧回调Frame Callback List │ │ └── requestAnimationFrame │ │ │ ├── 同步子操作task 内部实现细节 │ │ ├── DOM 解析 / 脚本编译执行 │ │ ├── Style Recalc / Layout / Paint │ │ ├── GC垃圾回收 │ │ ├── JIT 编译TurboFan │ │ └── eval() / inline script │ │ │ └── 根本不在主线程上 │ ├── Compositor 动画transform/opacity │ ├── 滚动/手势识别Compositor Thread │ ├── 光栅化Raster Threads │ ├── GPU 绘制提交GPU Process │ └── Worker / Service Worker 事件6.2 详细说明同步子操作——不是任务是任务的子步骤操作说明DOM 解析Parse HTML是某个 task 的内部行为样式计算读取 computed style 或渲染管线触发时同步执行布局Reflow读取几何属性或渲染管线触发时同步执行绘制Paint渲染管线的同步阶段GCV8 Minor/Major GC 在当前 task 期间同步触发Stop-The-WorldJIT 编译TurboFan 编译热点函数时同步阻塞脚本编译script的 parse compile 是同步操作Performance 面板中的超长灰色块就是这类同步操作它们不是task 在排队等待而是某个 task 内部执行过久。requestAnimationFrame——独立的帧回调列表rAF 回调存储在独立的Frame Callback List中不走 Task Queue也不走微任务队列。它在事件循环的“Update Rendering”步骤中批量执行与刷新率对齐每帧最多一次。合成线程任务——完全绕过主线程任务执行位置CSS transform/opacity 动画Compositor Thread滚动/手势Compositor Thread光栅化Raster ThreadsGPU 提交GPU Process这就是为什么主线程卡死时CSS 动画和滚动仍然流畅。七、事件循环单次迭代的完整执行顺序7.1 规范定义的固定步骤┌─────────────────────────────────────────────────────────┐ │ 事件循环 · 单次迭代 │ │ │ │ Step 1: Select Execute Task │ │ 从多个 TaskQueue 中按优先级选取一个 task 并执行 │ │ ↓ │ │ Step 2: Microtask Checkpoint │ │ 递归清空微任务队列Promise.then, MutationObserver等│ │ ↓ │ │ Step 3: Update Rendering? │ │ 浏览器判断是否需要渲染通常 ~60Hz │ │ ├── YES → 执行渲染流程见 7.2 │ │ └── NO → 跳过 │ │ ↓ │ │ Step 4: 回到 Step 1 │ └─────────────────────────────────────────────────────────┘⚠️ 这个步骤顺序固定不可变。优先级只在 Step 1 内部生效Step 2、Step 3 的执行时机由规范锁定。7.2 Update Rendering 阶段的内部顺序当浏览器判定需要渲染时按以下固定顺序执行Update Rendering 阶段 │ ├── 1. ResizeObserver 回调 ├── 2. scroll 事件派发渲染阶段的滚动通知 ├── 3. requestAnimationFrame 回调批量按注册顺序 ├── 4. IntersectionObserver 回调 ├── 5. Style Recalculation样式计算 ├── 6. Layout布局/回流 ├── 7. Paint绘制 └── 8. Composite合成提交到 GPU7.3 完整时间线示例时间线 → [Task P1: click handler] → [Microtasks: Promise.then × 3, MutationObserver × 1] → [Render Frame? YES] → [ResizeObserver] → [rAF callbacks] → [Style] → [Layout] → [Paint] → [Composite] → [Task P2: setTimeout callback] → [Microtasks: Promise.then × 1] → [Render Frame? NO] → [Task P2: fetch callback] → [Microtasks: empty] → [Render Frame? YES] → ... → [Task P3: requestIdleCallback] → [Microtasks: empty] → [No Render] → 线程挂起等待下一个事件...八、绝对优先级排序总表排名类型执行位置能否被抢占说明1MicrotaskStep 2Task 结束后同步清空❌ 不能被任何 Task 打断递归清空不让出控制权2P0 Control TaskStep 1❌ 最高优先级 Task内部任务3P1 User InteractionStep 1仅被 P0 抢占click/key/touch/scroll4P2 Default VisibleStep 1被 P0/P1 抢占timer/fetch/DOM解析/JS5rAF CallbackStep 3渲染阶段不参与 Task 竞争绑定帧节奏6ResizeObserver / IntersectionObserverStep 3渲染阶段绑定帧节奏在 rAF 前后7P3 Idle TaskStep 1仅在所有高优队列空闲时执行requestIdleCallback九、安全退出机制页面关闭时主线程如何安全退出boolkeep_runningtrue;voidMainThread(){for(;;){Task taskSelectNextTask();Execute(task);PerformMicrotaskCheckpoint();if(!keep_running)// 每次任务执行后检查退出标志break;}// 清理资源、销毁线程}确定退出时设置keep_running false当前正在执行的任务完成后即退出循环。十、常见误区纠正❌ 错误认知✅ 正确理解“微任务优先级高于宏任务”微任务和 Task不在同一维度。微任务在每个 Task 边界同步执行不参与 Task 队列的优先级竞争“宏任务是规范术语”规范只称task宏任务是社区俗称“rAF 是微任务”rAF 是渲染帧回调在 Step 3 执行与微任务完全无关“setTimeout(fn,0) 比 click 事件快”错。click 是 P1setTimeout 是 P2click 永远优先“所有 scroll 事件都是高优先级”JS scroll listener 是 P1 Task渲染阶段的 scroll 通知是 Step 3 固定步骤二者不同“Promise 比 MutationObserver 优先级高”同为微任务严格按 FIFO无优先级之分“requestIdleCallback 在微任务之后执行”rIC 是 P3 TaskStep 1微任务在 Step 2二者在不同步骤“JS 是单线程 浏览器是单线程”JS 引擎运行在主线程上但浏览器是多进程多线程架构“事件循环是 CPU 忙等待”底层是系统级中断无事件时线程休眠“CSS 动画和 JS 动画一样在主线程”部分 CSS 动画transform/opacity在合成线程执行不占主线程十一、Performance 面板实践验证打开 Chrome DevTools → Performance → 点击录制并刷新页面观察要点面板元素含义Main 轨道主线程所有任务执行流灰色长条单个 Task含内部同步子操作紫色块Parse HTMLHTML 解析。遇到script会暂停解析、同步执行 JS黄色块Compile ScriptJS 编译绿色块Recalculate Style / Layout渲染管线同步阶段橙色块Timer FiredsetTimeout/setInterval 回调帧Frames每 16.6ms 一帧可观察是否掉帧性能问题定位原则现象原因优化方向长灰色块50ms某个 Task 内部同步操作过长拆分长任务、使用 requestIdleCallback大量紫色/绿色块频繁触发样式计算/布局减少强制同步布局、批量 DOM 操作帧时间 16.6ms单帧内任务过多将非关键任务延后、使用 CSS 动画替代 JS 动画主线程卡死但滚动流畅长 Task 阻塞主线程合成线程动画不受影响属正常现象十二、实践指导速查场景推荐方案原因响应用户点击后立即更新 UI直接在 click handler 中同步操作 DOMP1 Task 紧随其后的渲染帧数据请求完成后更新视图fetch 回调 微任务批量合并 DOM 变更避免多次重排动画更新requestAnimationFrame绑定渲染帧不掉帧非紧急数据处理日志/分析requestIdleCallbackP3不阻塞用户交互和渲染DOM 变更后立即读取新尺寸MutationObserver微任务或ResizeObserver渲染阶段微任务保证同步性ResizeObserver 保证拿到最新布局避免微任务链过长卡死页面将长链拆分为多个 TasksetTimeout/MessageChannel让出控制权允许渲染和其他 Task 插入避免长 JS 阻塞渲染使用scheduler.yield()新API或setTimeout拆分允许渲染帧插入十三、一句话总结浏览器事件循环的本质是TaskSequenceManager 按优先级从多个 TaskQueue 中选取任务执行 → 执行完立即清空 V8 微任务队列 → 按帧节奏决定是否渲染更新 → 循环往复。微任务不在 Task Queue 中rAF 不在 Task Queue 中渲染管线是 Task 内部的同步子操作合成线程任务完全绕过主线程。附录推荐源码阅读路径顺序文件内容1base/task/sequence_manager/sequence_manager_impl.h调度器核心2base/task/sequence_manager/task_queue_impl.h单个队列实现3third_party/blink/renderer/core/scheduler/Blink 层队列注册与使用4v8/src/execution/microtask-queue.cc微任务队列实现5content/renderer/render_thread_impl.cc渲染进程主线程初始化6WHATWG HTML Standard - Event Loop规范原文以上文件可在 Chromium Code Search 直接浏览最新版本。