应使用 requestanimationframe 实现平滑数字增长动画,因其绑定浏览器刷新节奏、避免 setinterval 的跳值卡顿问题;需用 performance.now() 记录起始时间,progress 加 math.min 防超调,单次 dom 写入,并独立管理每个数字的动画状态。

用 requestAnimationFrame 做平滑增长,别用 setInterval
setInterval 在帧率不稳或页面失焦时容易跳值、卡顿,尤其在低端设备或后台标签页中,current += step 累加会严重偏离目标。而 requestAnimationFrame 绑定浏览器刷新节奏,能自然适配 60fps 或更高帧率。
- 动画起始时间必须用
performance.now()记录,不能靠Date.now(),后者受系统时间调整影响 - 进度计算要加
Math.min(..., 1)防止因帧延迟导致超调 - 每次更新只写一次
innerText,避免高频 DOM 写入引发重排
function animateNumber(el, start, end, duration = 2000) {
const startTime = performance.now();
const update = (timestamp) => {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
const value = Math.floor(start + (end - start) * progress);
el.innerText = value;
if (progress <h3>数字进视口才启动动画,用 <code>IntersectionObserver</code> 控制触发时机</h3>
直接在 <code>DOMContentLoaded</code> 里执行动画,用户还没看到数字就播完了。实际项目中,数字常位于页面中下部(如数据看板、统计模块),应等它进入视口再激活。
-
threshold: 0.1表示元素 10% 高度可见即触发,比0.5更早响应滚动 - 触发后立刻调用
observer.unobserve(el),避免重复执行 - 若数字是动态插入的(如 AJAX 渲染),需在插入后手动调用
observer.observe(el)
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animateNumber(entry.target, 0, parseInt(entry.target.dataset.target)); observer.unobserve(entry.target); } }); }, { threshold: 0.1 });
document.querySelectorAll('[data-target]').forEach(el => observer.observe(el));
处理带单位或后缀的数字,比如 “+1234” 或 “2.8万”
原始文本常含非数字字符,直接取innerText 会导致 NaN。得先剥离再还原。
- 用
textContent.replace(/\D/g, '')提纯数字,但要注意小数点和负号 - 保留原始后缀(如
+、万、%)需存到dataset或data-suffix - 动画结束时拼回去:
el.innerText = finalValue + (el.dataset.suffix || '')
const el = document.getElementById('counter'); const raw = el.textContent; const numMatch = raw.match(/-?\d+\.?\d*/); // 支持负数、小数 const baseValue = numMatch ? parseFloat(numMatch[0]) : 0; const suffix = raw.replace(numMatch?.[0] || '', '').trim();
animateNumber(el, 0, baseValue, 1500); // 动画结束后手动补后缀(因为中间过程不需要显示) el.addEventListener('animationend', () => { el.innerText = baseValue + suffix; });
多个数字同时动,别共享同一个定时器
常见错误是把所有counter 元素塞进一个循环,共用一个 requestAnimationFrame 回调 —— 一旦某个元素提前完成,整个回调就停了。
- 每个数字必须有独立的动画上下文(
startTime、start、end) - 不要用闭包变量复用
progress,每个update函数要闭包自己的状态 - 如果用类封装,确保每个实例的
this.startTime不被覆盖
document.querySelectorAll('.counter').forEach(el => { const target = parseInt(el.dataset.target); animateNumber(el, 0, target); // 每次调用都新建作用域 });
真实场景里最易被忽略的不是写法,而是数字格式化:千分位逗号、小数位截断、负数符号位置——这些必须在动画结束后统一处理,不能在每帧都做 toLocaleString(),否则性能暴跌。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











