
本文介绍如何使用原生 javascript 实现“懒启动”数字计数器——仅当用户滚动至目标区块进入视口时,才触发动画,避免页面加载即执行导致的无效计数。
在网页开发中,常见的数字计数器(如统计访问量、会员数、满意度等)若在页面加载时立即执行,而目标区块位于页面底部(如页脚附近),会导致用户尚未看到该区域时动画已结束,体验割裂且不直观。解决核心在于:将动画触发时机从 onload 改为“元素进入视口”的检测时机。
✅ 推荐方案:基于 getBoundingClientRect() 的视口检测
我们不再依赖 window.onload,而是监听 scroll 事件,并动态判断 .container 区块是否已进入用户可视区域(viewport)。关键原理是:
- element.getBoundingClientRect().top 返回元素上边界距离视口顶部的像素值(滚动时该值会从正变负);
- window.scrollY 表示当前垂直滚动距离(文档顶部到视口顶部的距离);
- 当 scrollY + window.innerHeight >= element.offsetTop 或更精准地用 getBoundingClientRect().top
但为简洁可靠,推荐使用以下优化实现(无需 jQuery,纯原生):
// 获取目标元素
const container = document.querySelector('.container');
const text1 = document.getElementById('0101');
const text2 = document.getElementById('0102');
const text3 = document.getElementById('0103');
// 动画函数(保持原逻辑,已优化兼容性)
function animate(obj, initVal, lastVal, duration) {
let startTime = null;
const step = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
obj.textContent = Math.floor(progress * (lastVal - initVal) + initVal);
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
// 防重复触发标志
let animationStarted = false;
// 滚动监听函数
function checkAndAnimate() {
const rect = container.getBoundingClientRect();
// 当容器上边缘进入视口(即 rect.top <= 视口高度)且未触发过时启动
if (rect.top <= window.innerHeight && !animationStarted) {
animate(text1, 0, 100, 3000);
animate(text2, 0, 300, 3000);
animate(text3, 0, 100, 3000);
animationStarted = true;
}
}
// 绑定滚动事件(建议节流以提升性能)
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
checkAndAnimate();
ticking = false;
});
ticking = true;
}
});
// 页面加载后立即检查一次(防止初始就在视口内)
checkAndAnimate();⚠️ 注意事项与最佳实践
- ID 命名规范:HTML 中 id='0101' 等纯数字 ID 不符合标准(HTML5 允许但易引发兼容性问题或 CSS 选择器歧义),建议改为语义化命名,如 id="counter-visits";
- 性能优化:直接监听 scroll 易造成高频调用,务必使用 requestAnimationFrame 节流(如上所示),避免卡顿;
- 兼容性增强:getBoundingClientRect() 在所有现代浏览器中均支持;若需支持旧版 IE,可改用 element.offsetTop 配合 scrollY 计算,但精度略低;
- 更优替代方案:生产环境推荐使用 Intersection Observer API,它专为这类场景设计,性能更佳、代码更简洁:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !animationStarted) {
animate(text1, 0, 100, 3000);
animate(text2, 0, 300, 3000);
animate(text3, 0, 100, 3000);
animationStarted = true;
observer.unobserve(container); // 动画后停止监听
}
});
}, { threshold: 0.1 }); // 当 10% 区域可见时触发
observer.observe(container);✅ 总结
让计数器“按需启动”,本质是将动画逻辑与用户行为(滚动)解耦,并通过视口检测建立精准触发条件。相比简单监听 onscroll,采用 IntersectionObserver 或带节流的 getBoundingClientRect 方案,既能保证用户体验流畅,又具备良好的可维护性与扩展性。记住:好的动画不是“一上来就动”,而是“你看见它时,它才开始呼吸”。










