[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的参数 二:对比 三:海康镜头选型工具...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
小木的算法日记-多叉树的递归/层序遍历
🌲 从二叉树到森林:一文彻底搞懂多叉树遍历的艺术 🚀 引言 你好,未来的算法大神! 在数据结构的世界里,“树”无疑是最核心、最迷人的概念之一。我们中的大多数人都是从 二叉树 开始入门的,它…...

解析“道作为序位生成器”的核心原理
解析“道作为序位生成器”的核心原理 以下完整展开道函数的零点调控机制,重点解析"道作为序位生成器"的核心原理与实现框架: 一、道函数的零点调控机制 1. 道作为序位生成器 道在认知坐标系$(x_{\text{物}}, y_{\text{意}}, z_{\text{文}}…...

PydanticAI快速入门示例
参考链接:https://ai.pydantic.dev/#why-use-pydanticai 示例代码 from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel from pydantic_ai.providers.openai import OpenAIProvider# 配置使用阿里云通义千问模型 model OpenAIMode…...