Web 平台 API / 事件循环 · 任务、microtask,与那一圈的所有时机 待审核 6 页

事件循环 · 任务、microtask,与那一圈的所有时机

JavaScript 是单线程的,却要同时响应点击、运行定时器、等待网络——靠的是一个持续循环事件循环 (event loop)。它的规则可以概括为一句:取一个 macrotask 执行完 → 把 microtask 全部清空 → 到时机就渲染 → 再取下一个。 理解这一圈,就能解释「为什么 Promise.thensetTimeout(0) 先执行」「页面为什么卡顿」「Vue/React 为何不卡」。

本系列从这一圈本身出发,逐个拆解向队列提交任务的入口: 骨架 → macrotask 家族 → microtask → rAF/rIC → postTask → 落地。每页都在真实浏览器中执行, 单步观察队列的增删,并直观对比卡顿与流畅的差别。

先理解这一圈 the loop · 调用栈 + macrotask + microtask

一圈是怎么转的:同步 → microtask → 渲染 → 下一个 macrotask

把整段 script 视为第一个 macrotask,单步推进一轮事件循环:macrotask 队列、microtask 队列与渲染机会各自何时被取用。

逐个拆解向队列提交任务的入口 macrotask · setTimeout 之外还有谁

macrotask 大家族:不止 setTimeout,而且它们并不等价

macrotask 不止 setTimeout,也彼此不等价。实测五种任务的排队先后,以及 setTimeout 连环嵌套超过五层后的 4ms 钳制台阶。

microtask · 高优先级的小任务

microtask:在「下一个 macrotask」之前全部执行完

microtask 的三个特性——优先于 macrotask、把一轮同步改动批处理成一次回调、清空到底期间不渲染,全部在真实浏览器中执行验证。

rAF / rIC · 跟着画面走

两个特殊任务:渲染前的 rAF,空闲时的 rIC

rAF 在绘制前执行、rIC 在绘制后的空闲期执行。实测同帧 rAF 回调共用一个 timestamp,以及 rAF 排在同帧还是下一帧取决于谁排的它。

scheduler.postTask · 带优先级的 macrotask

Scheduler.postTask:给 macrotask 装上「优先级」

scheduler.postTask 给 macrotask 补上三档优先级与 TaskController 取消,与 setTimeout、requestIdleCallback 逐项对比,并介绍 scheduler.yield。

落地:框架与性能,都取决于时机的选择 applications · 框架与工程里的事件循环

落地:框架与性能优化,本质都是在与事件循环打交道

长任务切片、Vue nextTick 的 microtask 批量更新、React Scheduler 的 MessageChannel 时间切片、scroll 用 rAF 节流,以及它们与 INP 的关系。

一句话串起来

主线程只有一个,事件循环按「macrotask → microtask 清空 → 渲染 (rAF) → 空闲 (rIC)」逐圈运转 (见 先理解这一圈)。 向队列提交任务的入口分两类:macrotask一次取一个 (见 macrotask 大家族,且彼此有快慢之分),microtask一次清空到底 (见 microtask,会优先执行、会批处理、可能阻塞渲染); rAF / rIC 位于渲染前后两个特殊时机 (见 rAF / rIC);postTask 为 macrotask 补上了优先级 (见 Scheduler.postTask)。 工程上所有关于卡顿与流畅的取舍 (见 落地),本质都是「把代码放在这一圈的哪个时机、是否按时让出主线程」

相关链接