news 2026/4/18 10:04:45

React19事件调度的设计思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React19事件调度的设计思路

先说结论,React 选择 MessageChannel 完成事件调度,是因为它:

  • 属于宏任务(不会饿死浏览器:JavaScript 一直占着主线程,导致浏览器一直没有机会去做它必须做的事(渲染、响应输入、布局、绘制))
  • 延迟极低(接近微任务,但不会阻塞渲染)
  • 相较于 rAF 不绑定渲染帧
  • 可控、可中断、可让出主线程

一、React 调度和事件循环的密切联系

1、React 在“调度”什么?

React 调度的不是「事件」, React 调度的是:Fiber 渲染任务(render work)

也就是我上篇文章说过的这些东西:

  • beginWork
  • completeWork
  • diff
  • 构建 workInProgress Fiber 树

React Scheduler 的目标只有是:在不阻塞浏览器的前提下,尽可能多地推进 Fiber 渲染进度。

所以 Scheduler 需要满足:

  • 能反复被调用
  • 每次执行一小部分
  • 执行完就“让出主线程”

2、回忆浏览器事件循环

事件循环模型:

┌─────────────┐ │ 宏任务队列(Task) │ ← setTimeout / MessageChannel / rAF callback └─────┬───────┘ ↓ 执行 JS ↓ ┌─────────────┐ │ 微任务队列(一次性清空) │ ← Promise.then / queueMicrotask └─────┬───────┘ ↓ 清空所有微任务 ↓ 浏览器渲染(paint)

因此,为了满足上述 Scheduler 的需求,我们只能选择 Task(后续详细说明为什么最终选择了 MessageChannel)。

二、React Scheduler 源码(React 19)

packages/scheduler/src/forks/SchedulerHostConfig.default.js

核心逻辑(简化):

const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; function requestHostCallback() { port.postMessage(null); // 用 MessageChannel 来“自我唤醒” }

Scheduler 执行模型:

MessageChannel 回调触发 ↓ performWorkUntilDeadline ↓ while (还有任务 && 没超时) { 执行 Fiber work } ↓ 时间不够 → 再发一次 MessageChannel(MessageChannel 是“下一次调度 tick”的触发器)

三、为什么不用微任务(Promise / queueMicrotask)

假如 React 用微任务会发生什么?

Promise.resolve().then(workLoop)

问题 1:会阻塞渲染

微任务会在paint 之前全部执行完

意味着:

React 继续 work → work 里又调度微任务 → 浏览器:你先别画 → UI 卡死

这完全就是 Fiber 的“时间切片”的对立做法。

问题 2:微任务不可中断

  • 微任务一旦开始
  • 浏览器必须清空
  • React 无法“让出主线程”,更没法实现并发渲染

四、为什么不用 setTimeout

setTimeout 的问题不是“慢”,而是“不稳定”。

问题 1:最小延迟不可靠

  • HTML 标准:​最小 4ms(​HTML Living Standard — Last Updated 31 January 2026

    If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

    setTimeout 在嵌套层级超过 5 层,timeout(延时)如果小于 4ms,那么则会设置为 4ms,这个时差是 React 无法接受的。

  • 精度太粗(Scheduler:“当前帧还能不能再干 2ms 的活?”)

五、为什么不用 requestAnimationFrame(rAF)

1、rAF 被绑定到“渲染帧”

一帧 ≈ 16.6ms

但 React 的目标是:​只要主线程空一点,我就推进一点 Fiber;​而不是:“非要等下一帧”。

2、rAF 在后台不执行

浏览器会暂停 rAF(选择性跳过渲染帧),React 更新直接“冻结”!

六、还得是 MessageChannel ~

MessageChannel 是什么?

const channel = new MessageChannel(); // 两个频道端口,这两个端口可以相互通信 const port1 = channel.port1; const port2 = channel.port2; btn1.onclick = function(){ // port2 给 port1 发消息 port2.postMessage(content.value); } // port1 监听自己受到的消息 port1.onmessage = function(event){ console.log(`port1 收到了来自 port2 的消息:${event.data}`); }

MessageChannel 完美规避掉上述一系列缺点:

MessageChannel + shouldYield => 时间切片。

React 并不是“无脑跑”,而是每一小段都问一句:

shouldYield()

判断依据:

  • performance.now()
  • 帧预算
  • 用户输入是否 pending

如果该让出:

requestHostCallback() // 再发一个 MessageChannel return;
[宏任务] MessageChannel ↓ React 执行 Fiber work(2~5ms) ↓ shouldYield = true ↓ postMessage 再约一次 ↓ [浏览器有机会 paint / 处理输入] ↓ [下一次 MessageChannel]

七、彩蛋来咯

1、requestAnimationFrame

盲猜很多同学对于上面若干种不如 MessageChannel 的做法还不是很清楚,根本在于事件循环掌握的不好,我这里针对事件循环的**requestAnimationFrame**详细讲讲(其他知识点可以翻看我之前写的关于事件循环的文章,讲解的非常清楚)。

事件循环里面的requestAnimationFrame仅仅是一个跟着渲染帧走的“小弟”,有渲染才有 rAF:

  • 它不能“缩短”上一个 16.66ms 中 Task 的执行时间
  • 保证回调只会在“浏览器即将渲染下一帧之前”执行

因此如果上一帧的 Task 太重导致错过渲染窗口,浏览器会直接“丢帧”,而不是排队执行导致连锁累积卡顿(setTimeout 的做法)

rAF 回调永远不会挤占渲染时机,只会“对齐”渲染节奏

“丢帧”这个概念,对于数码产品经常关注的同学应该会非常熟悉。我们拿游戏“原神”举例子,帧率越高动画越流畅,而如果某一帧事件 Task 执行时间太长(超过 1 帧总时长),rAF 就不再执行,这帧就被自动“丢掉了”。而一些手机厂商为了弥补这个问题,所以就出现了手动“插帧”的做法。

一般地,1s 对应着 60 帧,而 1 帧就是 16.66ms。如果一个 Task 超过了 16.66ms,那么就占用了下一帧的时间,下一帧则不再 rAF/paint (出现丢帧)。但如果我们使用低帧率,假如使用 30 帧 1s,那么 1 帧就是 33.3ms,这样虽然画质变差了,但是动画流畅度确实更好了。

浏览器在一帧内要做的事情(简化):

JS Task(古老说法:宏任务) → 微任务 → rAF → 样式计算 → Layout → Paint → Composite → 屏幕显示

只要 JS Task 超过 ~16ms,浏览器就来不及渲染这一帧​,结果就是:

  • 这一帧直接没画出来(掉帧)
  • 用户看到卡顿

假设这样写动画:

setTimeout(step, 16)

发生了什么?

Task A (20ms) 超过 16ms ↓ setTimeout 回调排队 ↓ Task B (又 20ms) ↓ Task C ...

后果是:

  • 定时器只管时间,不管渲染(这是“时间驱动”,不是“渲染驱动”)
  • 回调会持续排队
  • 每一帧都被 JS Task 挤爆
  • 卡顿会累积 + 放大

如果改为 rAF:

requestAnimationFrame(callback) // “当浏览器准备开始下一次渲染之前,调用我”
while (true) { 1. 取一个 Task 执行(macro task) 2. 执行所有 microtasks 3. 【渲染检查点】(当前时间 - 上一帧渲染时间 < 16.66ms(60Hz)) - requestAnimationFrame - style / layout / paint }

当然,如果 Task 一直执行得太久,requestAnimationFrame一直得不到执行,本质上仍然是卡顿,而且是「主线程被长期占用型卡顿」。所以 rAF 并不能拯救被 JS 完全占死的主线程。

2、用时间轴演示卡顿

卡顿:场景一

类型一:JS 把主线程彻底占死(致命卡顿)

Task 200msTask 200msTask 200ms

结果:

  • rAF
  • Render
  • 输入响应
  • 页面假死

rAF 无解

类型二:单帧偶尔超时(可恢复卡顿)

Task 20ms(偶发)Task 5msTask 5ms

结果:

  • 掉 1 帧
  • 后续帧恢复
  • 动画继续

这是 rAF 的“主战场”

卡顿:场景二

假设场景

  • 屏幕 60Hz(16.6ms / 帧)
  • 每个动画 step 的 JS 执行18ms
  • 使用setTimeout(step, 16)

第 1 帧(已经开始出问题)

0ms Task: step 执行(18ms)18ms microtasks18ms ❌ 超过 16.6ms,无法渲染18ms setTimeout 已经到期 → 下一个 step 已在 Task 队列中

结果:没渲染,但 JS 没停

第 2 帧(开始积压)

18ms Task: step 执行(18ms)36ms microtasks36ms ❌ 又错过渲染36ms 下一个 step 继续排队

第 N 帧(雪崩)

Task → Task → Task → Task → Task18ms 18ms 18ms 18ms 18ms

表现为:

  • JS 一直在跑
  • 浏览器几乎没有 Render 机会
  • 页面看起来卡住不动
  • CPU 占满

setTimeout 只认:时间到了 → 执行回调

不管:

  • 主线程忙不忙
  • 能不能渲染
  • 用户是不是在滚动 / 点击

当一帧没画出来:

  • rAF:直接跳过
  • setTimeout:继续补执行(它会制造“补帧”)

这意味着:错过的帧会变成多余的 JS 工作量

3、用户体感 vs setTimeout

setTimeout(雪崩)

Task Task Task Task Task18ms 18ms 18ms 18ms

  • JS 连续霸占主线程
  • Render 几乎进不去
  • 页面“僵死”

requestAnimationFrame(稳定但慢)

step →(等下一帧)→ step →(等下一帧)→ step

  • 每帧最多执行一次
  • Render 之间有喘息
  • 页面还能响应输入
  • 动画只是低 FPS(这是“慢”,不是“死”)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 8:13:45

ESP32-CAM实战:基于SD卡与Web端的多模式图片存储方案

1. ESP32-CAM双存储方案设计思路 第一次拿到ESP32-CAM开发板时&#xff0c;我就被它小巧的体积和强大的功能吸引了。这个火柴盒大小的板子集成了Wi-Fi、蓝牙、摄像头接口和MicroSD卡槽&#xff0c;简直就是物联网项目的瑞士军刀。但在实际项目中&#xff0c;我发现单纯的本地存…

作者头像 李华
网站建设 2026/4/17 23:59:57

大模型知识蒸馏实战:8种高效策略解析与15篇论文代码精要

1. 知识蒸馏的核心原理与价值 知识蒸馏本质上是一种"师生学习"机制&#xff0c;通过让轻量级的学生模型模仿复杂教师模型的行为模式&#xff0c;实现知识迁移。这个过程就像老中医带徒弟——老师傅&#xff08;大模型&#xff09;通过病例诊断&#xff08;预测结果&…

作者头像 李华
网站建设 2026/4/18 3:46:03

MedGemma-X效果展示:支持中英文混合提问的双语影像理解能力

MedGemma-X效果展示&#xff1a;支持中英文混合提问的双语影像理解能力 1. 真实场景下的“医生式对话”体验 你有没有试过这样阅片&#xff1a;把一张胸部X光片拖进系统&#xff0c;直接问—— “左上肺野这个结节边缘毛糙吗&#xff1f;和去年片子比大小有变化没&#xff1f…

作者头像 李华
网站建设 2026/4/18 5:43:13

基于YOLOv11的智能硬币检测系统:从数据集构建到模型部署全流程解析

1. 硬币检测系统的现实需求与技术选型 硬币检测系统在现实生活中有着广泛的应用场景&#xff0c;从自动售货机的零钱找零到银行金融系统的硬币清分处理&#xff0c;都需要高精度、高效率的硬币识别技术。传统基于机械传感器的检测方式存在磨损严重、适应性差等问题&#xff0c;…

作者头像 李华
网站建设 2026/4/18 3:53:02

GLM-4v-9b商业应用案例:电商商品自动描述生成系统搭建

GLM-4v-9b商业应用案例&#xff1a;电商商品自动描述生成系统搭建 在电商运营中&#xff0c;一个常被低估却极其耗时的环节是——为每件商品撰写专业、吸引人且符合平台规则的详情页文案。人工撰写不仅成本高&#xff08;平均单商品30–60分钟&#xff09;&#xff0c;还面临风…

作者头像 李华
网站建设 2026/4/18 3:52:01

Proteus仿真51单片机电子琴设计与音乐播放实现

1. 电子琴设计基础与硬件搭建 想要用51单片机做个电子琴&#xff1f;这事儿其实没想象中那么难。我当年第一次做这个项目时&#xff0c;连示波器都不会用&#xff0c;现在回头看发现核心就三件事&#xff1a;搞懂发声原理、搭对电路、写对代码。咱们先从最基础的硬件连接说起。…

作者头像 李华