[React] 手动实现CountTo 数字滚动效果
这个CountTo组件npmjs里当然有大把的依赖存在,不过今天我们不需要借助任何三方依赖,造个轮子来手动实现这个组件。
通过研究其他count to插件我们可以发现,数字滚动效果主要依赖于requestAnimationFrame
通过js帧来让数字动起来,数字变化则是依赖于内部的easingFn
函数来每次计算。
首先声明组件props类型
interface Props {/*** 动画开始的值*/start?: number;/*** 目标值*/end: number;/*** 持续时间*/duration?: number;/*** 是否自动播放*/autoPlay?: boolean;/*** 精度*/decimals?: number;/*** 小数点*/decimal?: string;/*** 千分位分隔符*/separator?: string;/*** 数字前 额外信息*/prefix?: string;/*** 数字后 额外信息*/suffix?: string;/*** 是否使用变速函数*/useEasing?: boolean;/*** 计算函数*/easingFn?: (t: number, b: number, c: number, d: number) => number;/*** 动画开始后传给父组件的回调*/started?: () => void;/*** 动画结束传递给父组件的回调*/ended?: () => void;
}
除了end
是必要的,其他都是可选参数。
所以我们需要给组件默认值,防止没有参数时会报错。
同时写几个工具函数便于后面使用
export default function Index({end,start = 0,duration = 3000,autoPlay = true,decimals = 0,decimal = '.',separator = ',',prefix = '',suffix = '',useEasing = true,easingFn = (t, b, c, d) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b,started = () => {},ended = () => {},
}: Props) {const isNumber = (val: string) => {return !isNaN(parseFloat(val));};// 格式化数据,返回想要展示的数据格式const formatNumber = (n: number) => {let val = '';if (n % 1 !== 0) val = n.toFixed(decimals);const x = val.split('.');let x1 = x[0];const x2 = x.length > 1 ? decimal + x[1] : '';const rgx = /(\d+)(\d{3})/;if (separator && !isNumber(separator)) {while (rgx.test(x1)) {x1 = x1.replace(rgx, '$1' + separator + '$2');}}return prefix + x1 + x2 + suffix;};...
}
初始化数据
const [state, setState] = useState<State>({start: 0,paused: false,duration,});const startTime = useRef(0);const _timestamp = useRef(0);const remaining = useRef(0);const printVal = useRef(0);const rAf = useRef(0);const endRef = useRef(end);const endedCallback = useRef(ended);const [displayValue, setValue] = useState(formatNumber(start));// 定义一个计算属性,当开始数字大于结束数字时返回trueconst stopCount = useMemo(() => start > end, [start, end]);
动画的关键函数
const count = (timestamp: number) => {if (!startTime.current) startTime.current = timestamp;_timestamp.current = timestamp;const progress = timestamp - startTime.current;remaining.current = state.duration - progress;// 是否使用速度变化曲线if (useEasing) {if (stopCount) {printVal.current = state.start - easingFn(progress, 0, state.start - end, state.duration);} else {printVal.current = easingFn(progress, state.start, end - state.start, state.duration);}} else {if (stopCount) {printVal.current = state.start - (state.start - endRef.current) * (progress / state.duration);} else {printVal.current = state.start + (endRef.current - state.start) * (progress / state.duration);}}if (stopCount) {printVal.current = printVal.current < endRef.current ? endRef.current : printVal.current;} else {printVal.current = printVal.current > endRef.current ? endRef.current : printVal.current;}setValue(formatNumber(printVal.current));if (progress < state.duration) {rAf.current = requestAnimationFrame(count);} else {endedCallback.current?.();}};
执行动画的函数
const startCount = () => {setState({ ...state, start, duration, paused: false });rAf.current = requestAnimationFrame(count);startTime.current = 0;};
挂载时监听是否有autoPlay
来选择是否开始动画,同时组件销毁后清除requestAnimationFrame
动画;
useEffect(() => {if (autoPlay) {startCount();started?.();}return () => {cancelAnimationFrame(rAf.current);};}, []);
一些相关依赖的监听及处理
useEffect(() => {if (!autoPlay) {cancelAnimationFrame(rAf.current);setState({ ...state, paused: true });}}, [autoPlay]);useEffect(() => {if (!state.paused) {cancelAnimationFrame(rAf.current);startCount();}}, [start]);
最后返回displayValue
就可以了;
好了 我要开启五一假期了!
最后附上完整代码 –
'use client';import { useEffect, useMemo, useRef, useState } from 'react';interface Props {/*** 动画开始的值*/start?: number;/*** 目标值*/end: number;/*** 持续时间*/duration?: number;/*** 是否自动播放*/autoPlay?: boolean;/*** 精度*/decimals?: number;/*** 小数点*/decimal?: string;/*** 千分位分隔符*/separator?: string;/*** 数字前 额外信息*/prefix?: string;/*** 数字后 额外信息*/suffix?: string;/*** 是否使用变速函数*/useEasing?: boolean;/*** 计算函数*/easingFn?: (t: number, b: number, c: number, d: number) => number;/*** 动画开始后传给父组件的回调*/started?: () => void;/*** 动画结束传递给父组件的回调*/ended?: () => void;
}
interface State {start: number;paused: boolean;duration: number;
}
export default function Index({end,start = 0,duration = 3000,autoPlay = true,decimals = 0,decimal = '.',separator = ',',prefix = '',suffix = '',useEasing = true,easingFn = (t, b, c, d) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b,started = () => {},ended = () => {},
}: Props) {const isNumber = (val: string) => {return !isNaN(parseFloat(val));};// 格式化数据,返回想要展示的数据格式const formatNumber = (n: number) => {let val = '';if (n % 1 !== 0) val = n.toFixed(decimals);const x = val.split('.');let x1 = x[0];const x2 = x.length > 1 ? decimal + x[1] : '';const rgx = /(\d+)(\d{3})/;if (separator && !isNumber(separator)) {while (rgx.test(x1)) {x1 = x1.replace(rgx, '$1' + separator + '$2');}}return prefix + x1 + x2 + suffix;};const [state, setState] = useState<State>({start: 0,paused: false,duration,});const startTime = useRef(0);const _timestamp = useRef(0);const remaining = useRef(0);const printVal = useRef(0);const rAf = useRef(0);const endRef = useRef(end);const endedCallback = useRef(ended);const [displayValue, setValue] = useState(formatNumber(start));// 定义一个计算属性,当开始数字大于结束数字时返回trueconst stopCount = useMemo(() => start > end, [start, end]);const count = (timestamp: number) => {if (!startTime.current) startTime.current = timestamp;_timestamp.current = timestamp;const progress = timestamp - startTime.current;remaining.current = state.duration - progress;// 是否使用速度变化曲线if (useEasing) {if (stopCount) {printVal.current = state.start - easingFn(progress, 0, state.start - end, state.duration);} else {printVal.current = easingFn(progress, state.start, end - state.start, state.duration);}} else {if (stopCount) {printVal.current = state.start - (state.start - endRef.current) * (progress / state.duration);} else {printVal.current = state.start + (endRef.current - state.start) * (progress / state.duration);}}if (stopCount) {printVal.current = printVal.current < endRef.current ? endRef.current : printVal.current;} else {printVal.current = printVal.current > endRef.current ? endRef.current : printVal.current;}setValue(formatNumber(printVal.current));if (progress < state.duration) {rAf.current = requestAnimationFrame(count);} else {endedCallback.current?.();}};const startCount = () => {setState({ ...state, start, duration, paused: false });rAf.current = requestAnimationFrame(count);startTime.current = 0;};useEffect(() => {if (!autoPlay) {cancelAnimationFrame(rAf.current);setState({ ...state, paused: true });}}, [autoPlay]);useEffect(() => {if (!state.paused) {cancelAnimationFrame(rAf.current);startCount();}}, [start]);useEffect(() => {if (autoPlay) {startCount();started?.();}return () => {cancelAnimationFrame(rAf.current);};}, []);return displayValue;
}
相关文章:
[React] 手动实现CountTo 数字滚动效果
这个CountTo组件npmjs里当然有大把的依赖存在,不过今天我们不需要借助任何三方依赖,造个轮子来手动实现这个组件。 通过研究其他count to插件我们可以发现,数字滚动效果主要依赖于requestAnimationFrame 通过js帧来让数字动起来,…...

9.Admin后台系统
9. Admin后台系统 Admin后台系统也称为网站后台管理系统, 主要对网站的信息进行管理, 如文字, 图片, 影音和其他日常使用的文件的发布, 更新, 删除等操作, 也包括功能信息的统计和管理, 如用户信息, 订单信息和访客信息等. 简单来说, 它是对网站数据库和文件进行快速操作和管…...
redis之集群
一.redis主从模式和redis集群模式的区别 redis主从模式:所有节点上的数据一致,但是key过多会影响性能 redis集群模式:将数据分散到多个redis节点,数据分片存储,提高了redis的吞吐量 二.redis cluster集群的特点 数据分片 多个存储入…...

#9松桑前端后花园周刊-React19beta、TS5.5beta、Node22.1.0、const滥用、jsDelivr、douyin-vue
行业动态 Mozilla 提供 Firefox 的 ARM64 Linux二进制文件 此前一直由发行版开发者或其他第三方提供,目前Mozilla提供了nightly版本,正式版仍需要全面测试后再推出。 发布 React 19 Beta 此测试版用于为 React 19 做准备的库。React团队概述React 19…...
STM32中UART通信的完整C语言代码范例
UART(通用异步收发器)是STM32微控制器中常用的外设,用于与其他设备进行串行通信。本文将提供一个完整的C语言代码范例,演示如何在STM32中使用UART进行数据传输。 硬件配置 在开始编写代码之前,需要确保以下硬件配置&…...

【ITK统计】第一期 分类器
很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 公众号:VTK忠粉 前言 本文分享ITK中的分类器及其使用情况,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动力(^U^)ノ~YO 在统计分…...

51单片机两个中断及中断嵌套
文章目录 前言一、中断嵌套是什么?二、两个同级别中断2.1 中断运行关系2.2 测试程序 三、两个不同级别中断实现中断嵌套3.1 中断运行关系3.2 测试程序 总结 前言 提示:这里可以添加本文要记录的大概内容: 课程需要: 提示&#x…...
VUE 监视数据原理
1、如何监测对象中的数据? 通过setter实现监视,且要在new vue时就传入监测的数据 (1)对象中后加的属性,vue默认不做响应式处理 (2)如需给后添加的属性做响应式,请使用如下API&#x…...

Thinkphp使用dd()函数
用过Laravel框架的同学都知道在调试代码的时候使用dd()函数打印变量非常方便,在ThinkPHP6及以上的版本框架中也默认加上了这个函数。但是在ThinkPHP5或更低版本的框架中,dd 并不是一个内置的方法,不过我们可以手动添加这个函数,步…...

Git使用指北
目录 创建一个Git仓库本地仓库添加文件文件提交到本地仓库缓冲区添加远程仓库地址本地仓库推送到远程仓库创建新的分支拉取代码同步删除缓冲区的文件,远程仓库的文件.gitignore文件 创建一个Git仓库 Git仓库分为远程和本地两种,远程仓库如Githu上创建的…...

STM32G030F6P6TR 芯片TSSOP20 MCU单片机微控制器芯片
STM32G030F6P6TR 在物联网(IoT)设备中的典型应用案例包括但不限于以下几个方面: 1. 环境监测系统: 使用传感器来监测温度、湿度、气压等环境因素,并通过无线通信模块将数据发送到中央服务器或云端平台进行分析和监控。…...

零基础入门学习Python第二阶01生成式(推导式),数据结构
Python语言进阶 重要知识点 生成式(推导式)的用法 prices {AAPL: 191.88,GOOG: 1186.96,IBM: 149.24,ORCL: 48.44,ACN: 166.89,FB: 208.09,SYMC: 21.29}# 用股票价格大于100元的股票构造一个新的字典prices2 {key: value for key, value in prices.i…...

Java面试题:多线程3
CAS Compare and Swap(比较再交换) 体现了一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性. 线程A和线程B对主内存中的变量c同时进行修改 在线程A中存在预期值a,修改后的更新值a1 在线程B中存在预期值b,修改后的更新值b1 当且仅当预期值和主内存中的变量值相等…...
【QEMU系统分析之实例篇(十八)】
系列文章目录 第十八章 QEMU系统仿真的机器创建分析实例 文章目录 系列文章目录第十八章 QEMU系统仿真的机器创建分析实例 前言一、QEMU是什么?二、QEMU系统仿真的机器创建分析实例1.系统仿真的命令行参数2.创建后期后端驱动qemu_create_late_backends()qtest_serv…...

pyside6的调色板QPalette的简单应用
使用调色板需要先导入:from PySide6.QtGui import QPalette 调色板QPalette的源代码: class QPalette(Shiboken.Object):class ColorGroup(enum.Enum):Active : QPalette.ColorGroup ... # 0x0Normal : QPalette.ColorGrou…...

苍穹外卖项目
Day01 收获 补习git Git学习之路-CSDN博客 nginx 作用:反向代理和负载均衡 swagger Swagger 与 Yapi Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。 项目接口文档网址: http://localhost:8080/doc.html Da…...
error: Execution was interrupted, reason: signal SIGABRT
c json解析时, error: Execution was interrupted, reason: signal SIGABRT const Json::Value points root["shapes"]; if (points.isArray()) { for (unsigned int i 0; i < points.size(); i) { std::cout << " - [" <<…...

HarmaonyOS鸿蒙应用科普课
一、什么是鸿蒙OS? 1.概念: 先给大家讲讲今天讲课的主题,鸿蒙OS是什么?鸿蒙系统大家都知道,就是一个操作系统,我们未来是为的成为鸿蒙程序员。所以我们不要将鸿蒙os完全等同于手机操作系统,太…...

数码管的显示
静态数码管显示 数码管有两种一种的负电压促发,一种是正电压促发,上图是单数码管的引脚 上图是数码管模组的引脚,采用了引脚复用技术 咱们这个单片机由8个单数码管,所以要用上38译码器,如下图 74138使能端,单片机上电直接就默认接通了 74HC245的作用是稳定输入输出,数据缓冲作…...

关于海康相机和镜头参数的记录
对比MV-CS020-10UC和大家用的最多的MV-CS016-10UC 其实前者适合雷达站使用,后者适合自瞄使用 一:MV-CS020-10UC的参数 二:对比 三:海康镜头选型工具...

AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

DAY 47
三、通道注意力 3.1 通道注意力的定义 # 新增:通道注意力模块(SE模块) class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...