React 调度器优化:源码中对任务队列使用最小堆(Min-Heap)而不是排序数组的根本原因是什么?

news2026/4/30 9:20:46
React 调度器优化为什么我们要用“堆”来排队而不是每次都“排序”——一场关于 CPU 节约的深度解剖大家好我是你们的老朋友今天咱们不聊组件怎么写也不聊 Hooks 的坑咱们来聊聊 React 最底层的那个“管家”——调度器。在 React 的世界里调度器就像是一个超级忙碌的餐厅经理。它手里拿着一份长长的“待办事项清单”任务队列上面写着各种任务比如“渲染这个页面”、“更新这个状态”、“执行这个 Effect”。问题来了这个经理是个急性子而且用户输入的速度极快任务来得跟不要钱一样。于是咱们面临一个经典的数据结构问题如何高效地处理这个任务队列在很长一段时间里或者说在 React 的早期版本里那个“老派”的经理可能会选择一种最直观、最粗暴的方法每次来了新任务先把现有的清单全打乱按优先级排个序然后拿最上面的那个。这种做法我们叫它“排序数组”。但后来React 团队觉得这太浪费 CPU 了于是他们换了个更聪明的工具最小堆。今天咱们就剥开 React 源码的外衣看看为什么 React 调度器要死磕这个“堆”而不是用更简单的“排序”。一、 “老派”作风的代价每次都排序累不累咱们先来模拟一下那个“老派”经理的工作流程。假设你的任务队列里现在有 10 个任务。这些任务都有截止时间比如expirationTime。截止时间越早优先级越高。老派做法用户输入了来了个新任务 A。经理把任务 A 加到队尾变成 11 个任务。经理大喊一声“所有人停一下”经理把 11 个任务全部拿出来用Array.sort()重新排个序。经理拿走排在第一个最早到期的任务去执行。执行完了再来个新任务 B……重复上述步骤。听起来很合理对吧但这有个大问题效率低得令人发指。咱们来算笔账。假设你每秒要处理 100 次调度这在现代 Web 应用中太常见了想想用户疯狂点击按钮的场景。排序的时间复杂度是 O(N log N)。这就像是每次你要找队头都要把整个队伍重新整一遍。插入的时间复杂度是 O(1)加到队尾但查询时间复杂度变成了 O(N log N)因为要排序。如果你的任务队列有 100 个任务每次插入都要进行大约100 * log(100) ≈ 660次比较操作。每秒 100 次调度就是每秒 66,000 次比较。这还没算上排序算法内部可能涉及的交换操作。这就像什么呢就像你点外卖每次来了新订单老板都要把店里所有的订单全部拿下来按距离重新叠一遍只为了找出离你最近的那一个。这老板要是还能活着那一定是身强体壮。React 的调度器可是要处理成千上万个 Fiber 节点的如果每次都用排序那 UI 渲染还没开始CPU 就因为忙着排序而卡死了。二、 “新派”智慧最小堆的逻辑那么React 的调度器是怎么做的呢它使用了最小堆。听着很高级其实没那么玄乎。最小堆本质上就是一个完全二叉树。想象一下你手里有一堆数字你要把它们排好序并且随时能取到最小的那个。如果你用数组你需要排序。如果你用堆你只需要保证一个规则父节点的值总是小于或等于它的子节点。这就好比一个等级森严的家族爷爷最大儿子们比爷爷小孙子们比儿子们小。为什么这能优化插入Push不需要排序整个树只需要把新元素放到树的最后然后像“上浮”一样跟它的父节点比较。如果它比父节点小就交换位置。这个过程只需要 O(log N) 的时间。因为树的高度是 logN你最多只需要往上走几层。取出Pop树的根节点索引 1永远是最小的那个元素。你直接拿走它就行不需要遍历整个数组。取走根节点后把最后一个元素移到根节点然后像“下沉”一样跟它的子节点比较把小的那个换上去。这依然只需要 O(log N) 的时间。所以React 调度器的核心策略就是插入 O(log N)取出 O(1)。这简直是针对高频插入场景的完美武器。三、 源码深挖React Scheduler 的堆实现咱们直接看 React 源码以 React 18 为例packages/scheduler/src/SchedulerMinHeap.js。React 里的堆并不是一个复杂的类它其实就是两个数组和一个计数器。// 简化版 React 堆实现 const heap []; let heapSize 0; // 比较函数判断 a 是否比 b 应该排在前面a 的 expirationTime 更早 function compare(a, b) { return a.expirationTime - b.expirationTime; } // 1. 插入任务 function push(heap, node) { const size heapSize; heap[size] node; // 关键点从最后开始向上比较直到找到合适的位置 siftUp(heap, node, size); } function siftUp(heap, node, i) { let index i; while (index 0) { const parentIndex (index - 1) 1; // 父节点索引(i-1)/2使用位运算优化 const parent heap[parentIndex]; // 如果当前节点比父节点小优先级更高就交换 if (compare(node, parent) 0) { heap[index] parent; heap[parentIndex] node; index parentIndex; } else { break; } } } // 2. 取出任务 function pop(heap) { const first heap[0]; // 拿走根节点优先级最高的 const last heap[heapSize - 1]; heapSize--; // 队列减一 if (heapSize 0) { heap[0] last; // 把最后一个元素移到根节点 siftDown(heap, last, 0); // 关键点从根节点开始向下调整 } else { // 如果队列为空清空引用防止内存泄漏 heap[0] null; } return first; } function siftDown(heap, node, i) { let index i; const left (2 * index) 1; // 左子节点 const right (2 * index) 2; // 右子节点 let leftChild heap[left]; let rightChild heap[right]; // 逻辑如果当前节点比左子节点大且比右子节点也大那它就站错了位置 // 我们需要把最小的那个子节点“提拔”上来自己“下沉” while (left heapSize) { let leftIndex left; let rightIndex right; let child leftChild; // 如果右子节点存在且比左子节点小那就选右子节点作为候选 if (right heapSize compare(rightChild, leftChild) 0) { leftIndex right; rightIndex left; child rightChild; } // 比较当前节点和选出来的那个“小弟”谁更小 if (compare(node, child) 0) { // 如果自己更小说明位置对了不用动 break; } // 否则交换位置 heap[index] child; heap[leftIndex] node; // 递归处理下一层 index leftIndex; left (2 * index) 1; right (2 * index) 2; leftChild heap[left]; rightChild heap[right]; } }看到没这就是 React 调度器的核心。没有复杂的排序算法只有简单的“上浮”和“下沉”。四、 为什么不用普通的二叉树——完全二叉树的重要性你可能会问“老师普通的二叉树不也能存数据吗”普通的二叉树比如 AVL 树或红黑树虽然查找快但它们为了保持平衡插入和删除的操作极其复杂涉及到大量的旋转。对于 React 这种每秒要进行成千上万次插入和删除的场景来说旋转太重了。React 选择的是完全二叉树。什么是完全二叉树就是除了最后一层其他层都填满了最后一层从左到右依次排列。React 为什么喜欢完全二叉树数组存储因为是完全二叉树我们可以完美地用数组来存储它不需要复杂的指针操作也不需要维护左右子节点的引用。heap[1]是根heap[2]是左孩子heap[3]是右孩子。索引计算简单父节点i的左孩子是2*i右孩子是2*i 1。这简直是数学家的福音计算速度极快没有任何额外开销。五、 React 中的“花活儿”startTime 与 expirationTimeReact 的调度器不仅仅是插入和删除它还有更复杂的逻辑任务推迟。React 不希望一次性把所有任务都塞给浏览器。它希望浏览器在空闲的时候干活或者把紧急的任务插队到最前面。这就引入了两个关键的时间概念startTime任务真正开始执行的时间。expirationTime任务的截止时间过期时间。React 的调度逻辑是这样的如果任务还没到startTime就把它扔到堆里但是标记为“等待中”。如果任务到了expirationTime就把它标记为“过期”必须马上执行。如果任务已经过期或者当前时间 startTime那就把它拿出来执行。为什么这跟堆有关因为堆是基于expirationTime排序的。但是如果你想在堆中间“插入”一个新任务或者“删除”一个旧任务排序数组做不到但堆可以React 的push和pop操作完美地支持了这种动态的优先级调整。六、 深度剖析交换逻辑的细节咱们再仔细看看siftUp和siftDown的代码细节这体现了 React 团队对性能的极致追求。1. 索引计算const parentIndex (index - 1) 1;这里用了无符号右移。为什么要用位运算因为计算机处理位运算的速度比处理除法运算快得多。虽然在这个层面上差异微乎其微但在调度器这种高频循环中每一微秒都至关重要。2. 比较函数if (compare(node, parent) 0)React 的比较函数非常简单function compare(a, b) { return a.expirationTime - b.expirationTime; }如果a.expirationTime b.expirationTime说明a比b更紧急应该排在前面。注意这里用的是减法。如果两个任务的expirationTime相同呢React 会按照它们在队列中的顺序来虽然这属于边界情况但保证了稳定性。3. 循环终止条件在siftUp中一旦index 0循环就结束了。这意味着新元素已经升到了根节点或者它比根节点还大这不太可能除非你插入的是垃圾数据反正它找到了位置。在siftDown中一旦left heapSize说明它已经是叶子节点了不需要再下沉了。七、 场景模拟一场关于“输入法”的调度为了让你彻底明白咱们来模拟一个场景你在输入框里疯狂打字。时间 T0空队列。时间 T1输入了字符 ‘A’。调度器创建任务 AexpirationTime T1 5000ms。操作push(heap, A)。过程A 放在索引 0数组下标 0。计算父节点索引 -1。循环结束。堆里只有一个 A。时间 T2输入了字符 ‘B’。任务 BexpirationTime T1 3000ms。操作push(heap, B)。过程B 放在索引 1。计算父节点是 A索引 0。比较B 的截止时间比 A 早3000 5000。交换结果A 在索引 1B 在索引 0。时间 T3输入了字符 ‘C’。任务 CexpirationTime T1 1000ms。操作push(heap, C)。过程C 放在索引 2。父节点是 B索引 1。比较C 比 B 早。交换结果B 在索引 2C 在索引 1。现在检查 C 的父节点C 的父节点是 A索引 0。比较C 比 A 早。交换最终结果C 在索引 0A 在索引 1B 在索引 2。时间 T4浏览器空闲准备调度。操作pop(heap)。结果直接拿走索引 0 的 C。无需遍历如果是排序数组呢操作把 C 加到队尾然后调用sort()。过程遍历所有元素进行 O(N log N) 次比较和交换。结果同样拿到了 C但浪费了 CPU。八、 React Scheduler 的“微操”React 还做了什么除了用堆React 的调度器在源码里还做了很多“微操”来配合这个堆结构。1. 延迟执行React 不会每次push就立即去操作堆。有时候如果当前时间还没到任务的startTimeReact 会直接忽略这个插入或者把它放到一个“延迟列表”里。这大大减少了堆的维护压力。2. 提前退出在siftUp和siftDown中如果发现新任务的时间比当前队头的任务时间还晚或者已经过期很久了React 有时会选择不把它加入堆或者直接丢弃。这取决于具体的调度策略如requestIdleCallbackvsMessageChannel。3. 批量更新React 的堆不仅仅是存任务它还处理“批处理”。如果你连续调用了三次setStateReact 不会触发三次调度而是会把这三个任务合并成一个或者把它们的截止时间统一推迟。这进一步减少了堆的操作次数。九、 为什么不用优先队列——堆就是优先队列你可能会问“既然堆这么好为什么 React 不直接用现成的优先队列库比如PriorityQueue或者 Java 的PriorityQueue”因为 React 是用 JavaScript 写的虽然底层有 Flow 类型检查。JavaScript 没有原生的优先队列。如果你用 JavaScript 实现一个优先队列通常也就是封装一个堆。React 的调度器甚至比普通的优先队列还要“变态”。普通的优先队列只关注“最小值”而 React 的调度器关注的是时间窗口。React 的调度器需要处理startTime何时开始和expirationTime何时结束。这意味着它需要频繁地修改堆中元素的优先级或者根据时间窗口进行过滤。普通的堆算法如Heapify通常用于一次性构建堆或者静态插入。React 需要的是动态的、高频的、基于时间维度的堆操作。十、 总结为什么是 Min-Heap好了咱们总结一下。React 调度器使用最小堆而不是排序数组的根本原因可以归纳为以下三点极致的效率在高频操作每秒成百上千次插入下堆的 O(log N) 插入和 O(1) 查询对比排序数组的 O(N log N) 查询性能差距是数量级的。React 必须保证主线程不被算法本身占用太多。完美的适配性堆的完全二叉树结构天然适合用数组存储配合 React 的索引计算逻辑能够极快地找到父节点和子节点。这种底层优化的代码往往比封装好的类库更轻量、更可控。动态调度的需求React 的调度不仅仅是“插入”和“取出”它还涉及到基于时间expirationTime的动态优先级调整。堆结构允许我们在不重排整个队列的情况下高效地插入和调整任务这是排序数组做不到的。结语所以下次当你看到 React 那个复杂的调度器源码看到那一堆关于heap、siftUp、siftDown的函数时不要觉得它晦涩难懂。你可以把它想象成一个极其自律的图书管理员他手里拿着一个二叉树结构的架子。新书来了他不需要把整排书都搬下来重新排他只需要把新书插到正确的位置或者把最旧的书取下来。这种对资源的极致节约正是 React 能够在复杂的单线程 JavaScript 环境中依然保持流畅动画和高性能渲染的秘密武器。这就是为什么我们要用堆而不是排序。因为在这个分秒必争的浏览器世界里聪明就是省钱。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2545091.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…