news 2026/5/1 11:48:24

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 23:30:51

Degrees of Lewdity 游戏本地化高效解决方案:从需求分析到质量验证

Degrees of Lewdity 游戏本地化高效解决方案&#xff1a;从需求分析到质量验证 【免费下载链接】Degrees-of-Lewdity-Chinese-Localization Degrees of Lewdity 游戏的授权中文社区本地化版本 项目地址: https://gitcode.com/gh_mirrors/de/Degrees-of-Lewdity-Chinese-Local…

作者头像 李华
网站建设 2026/5/1 6:10:55

造相 Z-Image 参数详解:Guidance Scale=0为何能启用Turbo模式?深度解析

造相 Z-Image 参数详解&#xff1a;Guidance Scale0为何能启用Turbo模式&#xff1f;深度解析 1. 造相 Z-Image 模型概述 造相 Z-Image 是阿里通义万相团队开源的文生图扩散模型&#xff0c;拥有20亿级参数规模&#xff0c;原生支持768768及以上分辨率的高清图像生成。该模型…

作者头像 李华
网站建设 2026/5/1 7:20:25

PDF-Parser-1.0小白指南:轻松搞定PDF文档分析

PDF-Parser-1.0小白指南&#xff1a;轻松搞定PDF文档分析 你是不是也遇到过这些情况&#xff1f; 打开一份几十页的PDF技术白皮书&#xff0c;想快速找到关键参数表格&#xff0c;结果复制粘贴全是乱码&#xff1b; 收到客户发来的带公式的调研报告&#xff0c;手动抄录公式又…

作者头像 李华
网站建设 2026/5/1 8:44:04

立知多模态重排序模型lychee-rerank-mm:5分钟快速部署教程

立知多模态重排序模型lychee-rerank-mm&#xff1a;5分钟快速部署教程 你有没有遇到过这样的问题&#xff1a;搜索结果“找得到”&#xff0c;但“排不准”&#xff1f;用户搜“猫咪玩球”&#xff0c;系统返回了10条图文&#xff0c;可最贴合的那张高清动图却排在第7位&#…

作者头像 李华
网站建设 2026/5/1 5:58:47

SillyTavern:重新定义AI角色扮演体验的进阶指南

SillyTavern&#xff1a;重新定义AI角色扮演体验的进阶指南 【免费下载链接】SillyTavern LLM Frontend for Power Users. 项目地址: https://gitcode.com/GitHub_Trending/si/SillyTavern SillyTavern作为面向高级用户的LLM前端界面&#xff0c;通过深度定制化功能和沉…

作者头像 李华