滚动数字效果(Counting Animation)是前端开发中常见的交互需求,通常用于展示统计数据、价格、分数等动态变化的数值,实现这一效果的核心在于利用 JavaScript 的定时器或动画 API,在指定时间内将起始数值平滑过渡到目标数值。
核心实现原理
实现滚动数字主要依赖以下三个关键要素:
- 起始值与目标值:明确动画开始时的数字和结束时的数字。
- 持续时间:定义动画完成的总时长(毫秒)。
- 缓动函数(Easing Function):决定数字变化的速度曲线,如线性变化、先快后慢等,使动画更自然。
基础实现方案:使用 setInterval
这是最传统且兼容性最好的方法,通过定时器每隔固定时间更新一次 DOM 文本内容。
| 步骤 | 描述 | 代码逻辑示意 |
|---|---|---|
| 1 | 获取目标元素和参数 | 获取 DOM 节点,设置 start, end, duration |
| 2 | 计算每次增量 | step = (end start) / (duration / interval) |
| 3 | 启动定时器 | 使用 setInterval 每隔 16ms 或 10ms 执行一次 |
| 4 | 更新 DOM | 将当前计算出的数值格式化后写入 innerText |
| 5 | 清除定时器 | 当当前值达到目标值时,调用
停止动画 |
代码示例:
function animateValue(obj, start, end, duration) {
let startTimestamp = null;
const step = (timestamp) => {
if (!startTimestamp) startTimestamp = timestamp;
const progress = Math.min((timestamp startTimestamp) / duration, 1);
// 使用 easeOutQuad 缓动函数让动画更自然
const easeProgress = 1 (1 progress) (1 progress);
const currentVal = Math.floor(easeProgress (end start) + start);
obj.innerHTML = currentVal;
if (progress < 1) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
}
// 使用示例
const counter = document.getElementById("counter");
animateValue(counter, 0, 1000, 2000); // 从0滚动到1000,耗时2秒
注:虽然上述示例使用了 requestAnimationFrame,但其逻辑与 setInterval 类似,只是帧率更稳定,若严格使用 setInterval,需注意性能损耗和精度问题。
进阶实现方案:使用 requestAnimationFrame
相比 setInterval,requestAnimationFrame 能更好地与浏览器刷新率同步(通常为 60fps),避免动画卡顿或掉帧,是更推荐的生产环境方案。
优势对比:
| 特性 | setInterval | requestAnimationFrame |
|---|---|---|
| 执行时机 | 固定时间间隔,可能错过渲染帧 | 浏览器下次重绘前执行,同步屏幕刷新率 |
| 性能 | 后台标签页中仍会执行,浪费资源 | 后台标签页中自动暂停,节省电量 |
| 精度 | 受 JS 事件循环影响,可能有延迟 | 更高精度,动画更流畅 |
| 适用场景 | 简单逻辑、低性能要求 | 复杂动画、高性能要求、移动端优化 |
高级功能扩展
在实际项目中,往往需要处理更复杂的场景,如千分位格式化、小数支持、触发条件等。
-
数值格式化:
在更新 DOM 前,对数字进行千分位处理(如1,000)或保留小数位,可使用Intl.NumberFormat或正则表达式实现。 -
触发滚动:
通常希望数字在元素进入视口时才启动动画,而非页面加载时,可结合IntersectionObserverAPI 实现:const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const el = entry.target; const target = parseInt(el.dataset.target); animateValue(el, 0, target, 2000); observer.unobserve(el); // 只触发一次 } }); }); observer.observe(document.getElementById('counter')); -
支持小数与负数:
基础算法需修改为支持浮点数运算,并在格式化时保留指定小数位。
常见问题与解答
问题 1:为什么我的滚动数字动画在低端设备上会出现卡顿?
解答:
卡顿通常是因为动画逻辑阻塞了主线程,或者使用了 setInterval 导致频繁触发重排(Reflow),建议采取以下优化措施:

- 使用
requestAnimationFrame替代setInterval,确保动画与屏幕刷新率同步。 - 避免在动画回调中操作复杂的 DOM 属性,仅更新
textContent或innerText。 - 如果数值变化范围极大,可简化计算逻辑,或使用 Web Worker 在后台线程计算(适用于极复杂场景)。
- 检查是否触发了布局重排,确保只修改样式或文本内容,不修改影响布局的属性(如 width、height)。
问题 2:如何实现数字滚动到目标值后,再次点击按钮重新从 0 开始滚动?
解答:
需要确保动画函数是可重入的(Reentrant),关键在于正确管理定时器或动画帧的引用,并在每次启动新动画前清除旧的动画状态。
具体实现步骤:
- 在
animateValue函数内部,使用window.requestAnimationFrame时,无需手动清除,因为每次调用都会创建新的动画帧循环。 - 但如果使用
setInterval,必须在全局或闭包中保存intervalId,并在每次启动新动画前调用clearInterval(intervalId)。 - 重置 DOM 内容:在启动新动画前,将元素内容重置为起始值(如 0)。
- 绑定点击事件:在按钮的
click事件中,先重置数值,再调用animateValue。
示例代码片段:
let currentAnimationId = null;
function resetAndAnimate(el, start, end, duration) {
// 如果有正在进行的动画,取消它
if (currentAnimationId) {
cancelAnimationFrame(currentAnimationId);
}
el.innerHTML = start; // 重置显示
animateValue(el, start, end, duration);
}
// 在 animateValue 内部,将 requestAnimationFrame 的返回值赋给 currentAnimationId
// 以便在需要时取消
原创文章,发布者:酷盾叔,转转请注明出处:https://www.kd.cn/ask/461155.html