React 探秘(三): 时间切片
文章目录
- 背景
- 时间切片原理
- requestIderCallback 方法
- setImmediate
- MessageChannel
- setTimeout
- React 18 时间切片源码
- 手撸时间切片
- 问题拆解
- 构建任务队列
- 宏任务包装
- 首次开启任务
- 递归任务执行
- workLoop 开启工作循环
- demo 模拟
- 总结
背景
前文学习了 fiber 架构和双缓存技术,接下来我们深入源码一起学习下时间切片的原理。
React 探秘(一):fiber 架构
React 探秘(二):双缓存技术
React 时间切片是 React 通过将任务分割成小的时间片,然后分批次去处理任务,在 js 线程繁忙的时候把控制权交还给浏览器本身如渲染进程等,以提高应用程序性能的一种技术。本文将介绍 React v18.3.1
时间切片并提供一个简单的 demo,以便开发者学习相关知识。
时间切片的主要优点:
提高应用程序的响应性和流畅度,分批次运行任务可以避免长时间占用 CPU。更好地控制渲染过程,让用户可以快速看到应用程序的变化,避免白屏等问题。
时间切片技术位置 fiber
架构的 Scheduler
调度器层。
Scheduler
分为两大部分:
- 时间切片: 异步渲染是优先级调度实现的前提
- 优先级调度:在异步渲染的基础上引入优先级机制控制任务的打断、替换。
本文只介绍时间切片相关内容;
时间切片原理
时间切片的原理就是把我们一次性执行完的任务,切分到不同时间间隔去完成,如果超出这个时间间隔,就会暂时挂起,交给浏览器,等到空闲了继续执行。那么问题就转化为如何实现给任务添加时间间隔?
这里涉及到 js 事件循环机制,同步代码(宏任务)-微任务-宏任务。
- 执行全局代码:当 JavaScript 代码第一次运行时,首先会执行同步代码(相当于一次宏任务),如果遇到微任务会把微任务方微任务队列,遇到宏任务放入宏任务队列
- 检查微任务队列:一旦同步代码(宏任务)完成,事件循环会检查并执行微任务队列中的所有任务,直到队列为空。
- 执行下一个宏任务:如果微任务队列为空,事件循环会从宏任务队列中取出下一个任务并执行。
- 重复上述步骤:这个过程会不断循环,直到所有任务执行完毕。
宏任务:会在下次事件循环中执行,不会阻塞本次页面渲染更新。
微任务:「微任务是在本次页面更新前会全部执行」,这一点与同步执行无异,不会让出主线程。
常见的宏任务方法有:
setTimeout
messageChannel
setImmediate
此外还有 requestIdleCallback
是在浏览器渲染后有空闲时间时执行。
requestIderCallback 方法
window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout
,则有可能为了在超时前执行函数而打乱执行顺序。
通过这个函数我们其实就可以时间一个简单的时间切片:
function workLoop(deadline) {let shouldYield = false;// 存在fiber并且时间空闲while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1; // 剩余时间是否小于1ms 代表任务繁忙}// 没有fiber并且wip存在if (!nextUnitOfWork && workInProgress) {commitRoot();}// 繁忙时继续执行主任务requestIdleCallback(workLoop);
}
我们执行某个 fiberNode
的时候,浏览器主线程被占用,这个时候就可以暂停 fiberNode
的继续执行,等浏览器空闲时,继续 nextUnitOfWork
。这就实现了可暂停可继续。但是呢这 api 有限制:
requestIdleCallback
的执行时机不是完全可控的,这可能导致在不同环境中表现不一致。requestIdleCallback
是利用帧之间空闲时间来执行js
,它是一个低优先级的处理策略,但实际上fiber
的处理上,并不算是一个低优先级任务。
setImmediate
setImmediate
这个是最早执行的宏任务,但是也可能会有兼容性问题。
MessageChannel
MessageChannel
的执行时机比 setTimeout
靠前,而且执行实际准确,但是会有兼容性问题。
setTimeout
setTimeout
执行时机在 messageChannel
之后,如下 demo:
function workLoop() {setTimeout(() => {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);workLoop()}, 0)
}
但是 setTimeout
的递归层级过深的话,延迟就不是1ms,而是4ms,这样会造成延迟时间过长,时间浪费。
看了上面这些方法多多少少都有些问题,那么下面我们讲一下 react 怎么实现时间切片的。
React 18 时间切片源码
源码位置: https://github.com/facebook/react/blob/v18.3.1/packages/scheduler/src/forks/Scheduler.js
可以看到 react 18
中其实就是做了个兼容性判断
- 优先
setImmediate
- 其次
messageChannel
- 最后
setTimeout
直接看源码很容易懵逼,因为源码中包含大量的兼容判断和优先级相关代码,容易混淆我们的视线,因此我们把复杂问题拆解一下,从源码入手,手撸一个 mini
版时间切片。
手撸时间切片
问题拆解
- 入口构建任务队列
-
- 创建时间切片,通过当前时间 + 延迟得到过期时间,塞入任务队列
- 创建宏任务
-
- 通过
setImmediate
等方法创建宏任务。
- 通过
- 执行宏任务-循环执行时间切片
-
- 递归调用时间切片方法,用于挂起、重启。
- 开启工作循环
-
- 循环执行队列任务,超出时间不执行。
构建任务队列
使用 performance.now()
获取更精确的时间,来创建每个任务过期时间,并塞入任务队列中。
// 入口创建 task 并添加过期时间,执行任务
function scheduleCallback(callbcak) {let unitOfwork = {callbcak,expirationTime: performance.now() + 5,}taskQueue.push(unitOfwork)// 开启宏任务requestHostCallback(workLoop)
}
宏任务包装
通过如下三个方法 localSetImmediate MessageChannel localSetTimeout
包装我们的 callback
为宏任务
// 把 performWorkUntilDeadline 方法放入宏任务当中
if (typeof localSetImmediate === 'function') {schedulePerformWorkUntilDeadline = () => {localSetImmediate(performWorkUntilDeadline);};
} else if (typeof MessageChannel !== 'undefined') {const channel = new MessageChannel();const port = channel.port2;channel.port1.onmessage = performWorkUntilDeadline;schedulePerformWorkUntilDeadline = () => {port.postMessage(null);};
} else {schedulePerformWorkUntilDeadline = () => {localSetTimeout(performWorkUntilDeadline, 0);};
}
首次开启任务
拿到当前正在处理的任务,开启执行包装好的宏任务
function requestHostCallback(callback) {scheduledHostCallback = callback;if (!isMessageLoopRunning) {isMessageLoopRunning = true;schedulePerformWorkUntilDeadline();}
}
递归任务执行
执行宏任务,获取当前时间,判断如果还有未完成的任务则开启递归。
// 宏任务执行的方法(核心方法)
const performWorkUntilDeadline = () => {if (scheduledHostCallback !== null) {const currentTime = getCurrentTime();startTime = currentTime;const hasTimeRemaining = true;let hasMoreWork = true;try {// 执行任务 scheduledHostCallback 就是 workLoophasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);} finally {if (hasMoreWork) {// 如果任务队列中还存在任务则继续递归执行schedulePerformWorkUntilDeadline();} else {isMessageLoopRunning = false;scheduledHostCallback = null;}}} else {isMessageLoopRunning = false;}
};
workLoop 开启工作循环
循环执行队列中的任务,currentTask
为空结束循环,判断时间是否过期,过期则不执行任务,把控制权还给浏览器。
function workLoop(hasTimeRemaining, initialTime) {let currentTime = initialTime;currentTask = peek(taskQueue);while (currentTask) {// 判断是时间是否过期if ((currentTask.expirationTime > currentTime) && (shouldYieldToHost() || !hasTimeRemaining)) {break} else {// 执行具体回调currentTask.callbcak()currentTask = taskQueue.shift()// currentTask = peek(taskQueue); // react 18 写法 包含小顶堆的排序算法}}// 还有剩余任务未执行完成返回 trueif (currentTask !== null) {return true;} else {return false;}
}
demo 模拟
下面使具体案例来模拟一下时间切片带来的改善:
完整版时间切片方法:
let taskQueue = [] // 任务队列
let isMessageLoopRunning = false; // 标记 宏任务 正在运行
let scheduledHostCallback = null; // 要执行的函数 workLoop
let currentTask = null; // 当前执行的任务
let startTime = null; // 任务开始的时间const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =typeof setImmediate !== 'undefined' ? setImmediate : null;// 获取当前时间
const getCurrentTime = () => performance.now();
// 根据时间判断是否把控制权交给浏览器
function shouldYieldToHost() {const timeElapsed = getCurrentTime() - startTime;if (timeElapsed < 5) {return false;}return true;
}
// 获取数组第一项
function peek(heap) {return heap.length === 0 ? null : heap[0];
}
// 入口创建 task 并添加过期时间,执行任务
function scheduleCallback(callbcak) {let unitOfwork = {callbcak,expirationTime: performance.now() + 5,}taskQueue.push(unitOfwork)// 开启宏任务requestHostCallback(workLoop)
}// 宏任务执行的方法(核心方法)
const performWorkUntilDeadline = () => {if (scheduledHostCallback !== null) {const currentTime = getCurrentTime();startTime = currentTime;const hasTimeRemaining = true;let hasMoreWork = true;try {// 执行任务 scheduledHostCallback 就是 workLoophasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);} finally {if (hasMoreWork) {// 如果任务队列中还存在任务则继续递归执行schedulePerformWorkUntilDeadline();} else {isMessageLoopRunning = false;scheduledHostCallback = null;}}} else {isMessageLoopRunning = false;}
};// 把 performWorkUntilDeadline 方法放入宏任务当中
if (typeof localSetImmediate === 'function') {schedulePerformWorkUntilDeadline = () => {localSetImmediate(performWorkUntilDeadline);};
} else if (typeof MessageChannel !== 'undefined') {const channel = new MessageChannel();const port = channel.port2;channel.port1.onmessage = performWorkUntilDeadline;schedulePerformWorkUntilDeadline = () => {port.postMessage(null);};
} else {schedulePerformWorkUntilDeadline = () => {localSetTimeout(performWorkUntilDeadline, 0);};
}function requestHostCallback(callback) {scheduledHostCallback = callback;if (!isMessageLoopRunning) {isMessageLoopRunning = true;schedulePerformWorkUntilDeadline();}
}function workLoop(hasTimeRemaining, initialTime) {let currentTime = initialTime;currentTask = peek(taskQueue);while (currentTask) {// 判断是时间是否过期if ((currentTask.expirationTime > currentTime) && (shouldYieldToHost() || !hasTimeRemaining)) {break} else {// 执行具体回调currentTask.callbcak()currentTask = taskQueue.shift()// currentTask = peek(taskQueue); // react 18 写法 包含小顶堆的排序算法}}// 还有剩余任务未执行完成返回 trueif (currentTask !== null) {return true;} else {return false;}
}
demo
模拟实现:
let taskIndex = 0;
let taskTotal = 5000; // 任务数量
const start = Date.now();function handleTask() {for (let j = 0; j < 5000; j++) {// 执行一些耗时操作const btn1Attr = document.getElementById('btn1').attributes;const btn2Attr = document.getElementById('btn2').attributes;const btn3Attr = document.getElementById('btn3').attributes;}if(taskIndex >= taskTotal) {console.log(`任务调度完成,用时:`, Date.now() - start, 'ms!');}
}while (taskIndex <= taskTotal) {scheduleCallback(handleTask) // 时间切片执行// handleTask() // 普通执行taskIndex++
}document.getElementById('btn1').onclick = function () {console.log(11111, 'click')
}// html
<body><div id="root"><button id="btn1">按钮1</button><button id="btn2">按钮2</button><button id="btn3">按钮3</button><button id="btn4">按钮4</button></div><script src="./sh.js"></script>
</body>
上面这一串代码在使用我们封装的 scheduleCallback
执行任务时,dom
渲染几乎秒开,但是如果使用普通的调用页面则会卡顿 3s
左右,才会出现。
总结
react
使用时间切片提升渲染性能,在熟知原理后,同样我们在业务中也有很多优化场景可以使用到。例如:高频埋点批量切片上传,大量 dom 节点操作等等。
相关文章:

React 探秘(三): 时间切片
文章目录 背景时间切片原理requestIderCallback 方法setImmediateMessageChannelsetTimeout React 18 时间切片源码手撸时间切片问题拆解构建任务队列宏任务包装首次开启任务递归任务执行workLoop 开启工作循环demo 模拟 总结 背景 前文学习了 fiber 架构和双缓存技术ÿ…...
OSError: Can‘t load tokenizer for ‘bert-base-uncased‘.
一、具体报错: 报错如下: OSError: Cant load tokenizer for bert-base-uncased. If you were trying to load it from https://huggingface.co/models, make sure you dont have a local directory with the same name. Otherwise, make sure bert-bas…...

中国人寿财险青岛市分公司:专业团队,卓越服务
中国人寿财险青岛市分公司拥有一支专业的团队,为客户提供卓越的保险服务。 公司的保险从业人员都经过严格的专业培训和考核,具备扎实的保险知识和丰富的实践经验。他们以客户为中心,用心倾听客户需求,为客户提供个性化的保险方案…...
【SpringCloud】基础问题
文章目录 spring-cloud-dependencies和spring-cloud-alibaba-dependencies的区别<dependencyManagement>和<dependencies>的区别<dependencyManagement><dependencies> 为什么在主函数上加上SpringBootApplication注解就可以扫描到对象为什么bootstrap…...

牛客网刷题(1)(java之数据类型、数组的创建(静态/动态初始化)、static关键字与静态属性和方法、常用的servlet包、面向对象程序设计方法优点)
目录 一、Java变量的数据类型。 <1>Java中变量的数据类型。 <2>基本数据类型。 <3>引用数据类型。 二、Java中一维数组的初始化。(静态、动态初始化) <1>数组。 <2>动态初始化。 <3>静态初始化。 三、看清代码后&am…...

电磁干扰(EMI)与电磁兼容性(EMC)【小登培训】
电磁干扰(EMI)和电磁兼容性(EMC)是每个产品在3C ,CE认证过程中必不可少的测试项目: 一、电磁干扰(EMI) EMI(Electromagnetic Interference)是指电子设备在工作…...

保险行业的智能客服:企业AI助理与知识库的加速效应
在保险行业,客户服务是企业与客户之间建立信任与忠诚度的关键桥梁。随着人工智能技术的飞速发展,企业AI助理正逐步成为保险客服领域的重要革新力量。 一、AI助理:保险客服的新篇章 企业AI助理,以其强大的自然语言处理能力、数据分…...

PSINS工具箱函数介绍——inserrplot
关于工具箱 i n s e r r p l o t inserrplot in...

龙蟠科技业绩压力显著:资产负债率持续攀升,产能利用率也不乐观
《港湾商业观察》施子夫 黄懿 去年十月至今两度递表后,10月17日,江苏龙蟠科技股份有限公司(以下简称,龙蟠科技;603906.SH,02465.HK)通过港交所主板上市聆讯。 很快,龙蟠科技发布公告称,公司全…...
使用 Spring Cloud 有什么优势?
使用 Spring Cloud 有什么优势? 在当今的微服务架构时代,Spring Cloud 作为一个强大的开发框架,备受开发者青睐。那么,使用 Spring Cloud 究竟有哪些优势呢? 一、微服务架构简介 微服务架构是一种将单一应用程序拆分…...

MySQL 日志之 binlog 格式 → 关于 MySQL 默认隔离级别的探讨
开心一刻 image 产品还没测试直接投入生产时,这尼玛... 背景问题 再讲 binlog 之前,我们先来回顾下主流关系型数据库的默认隔离级别,是默认隔离级别,不是事务有哪几种隔离级别,别会错题意了 1、Oracle、SQL Server 的默…...

SQL进阶技巧:Hive如何进行更新和删除操作?
目录 0 Hive支持更新和删除操作吗? 1 Hive删除操作如何实现? 2 Hive更新操作如何实现? 3 小结 0 Hive支持更新和删除操作吗? Hive在默认情况下不支持更新和删除操作,但可以通过特定方式如使用ORCFileformat和Acid…...
nginx安装详解含 自动化编译安装 Debian/Ubuntu/CentOS/RHEL/ROCKY
1. 准备工作 1.1 选择操作系统 推荐操作系统:Ubuntu、CentOS、Debian等Linux发行版。系统要求:确保服务器有足够的CPU、内存和磁盘空间。 1.2 更新系统 更新包列表: sudo apt update # 对于Debian/Ubuntu sudo yum update # 对于CentOS…...
Go编程语言介绍及项目案例
Go(又称 Golang)是一种开源的编程语言,具有高效、简洁、并发性能强等特点。 一、主要特点 简洁高效: Go 语言的语法简洁明了,代码风格清晰易读。它摒弃了一些传统编程语言中的复杂特性,如继承、泛型等,使得代码更加简洁高效。例如,在 Go 语言中,函数的定义非常简洁,…...
刷爆leetcode Day11 DFS
DFS 1. 汉诺塔(easy)2. 合并两个有序链表(easy)3. 反转链表(easy)4. 两两交换链表中的节点(medium)5. Pow(x,n)-快速幂(medium) 1. 汉诺塔&#x…...
虚拟机不同网络模式的区别
网络模式 NAT模式 (可以上网) 使用NAT模式的虚拟机都和物理机VMnet8处于同一个网段 桥接模式 (可以上网) 使用桥接模式的虚拟机都和物理机网卡处于同一网段 仅主机模式 (不能上网,完全隔离࿰…...
嵌入式软件 Bug 排查与调试技巧
目录 1、准备工作 2、打印调试 实现步骤 注意事项 3、断点调试 4、观察点调试 5、远程调试 6、内存分析 内存泄漏检测 栈溢出检测 7、异常处理 8、性能分析 9、逻辑分析仪 10、示波器 11、常见bug类型 12、调试策略 1、准备工作 硬件工具准备 调试器:例如 J - …...

阿里云环境下用docker搭建redis主从复制
redis主从复制可将主redis中的数据同步到从redis中,具有读写分离、容灾恢复、数据备份、支持高并发等特性。 本文演示在阿里云环境下,用 docker 搭建 redis 主从复制(一主二从)的操作过程。 一、环境准备 阿里云安装CentOS7.9 6…...
STM32 从0开始系统学习 1
笔者最近打算使用STM32系统的做一点东西。大二的时候就开始慢慢接触了STM32,拿他来做过一些事情,但是始终是葫芦吞枣,有点不扎实。笔者这里打算重新开始好好整理一下STM32的内容。 在这一笔记中很简单,就是解答一下啥是STM32&…...

python-numpy-笔记1
numpy官网NumPy User Guide 启动终端jupyter notebook 1.进入CMD输入jupyter notebook --enter--等待结束,使用Edge打开后出现jupyter的网页...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...