You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React实现滚动至视口时启动的单次递增计数器

React 实现视口触发的单次数字滚动计数效果

下面是一个通用的实现方案,满足元素进入视口启动计数、到指定值停止、仅执行一次的需求,代码可直接复用或按需调整:

import { useEffect, useRef, useState } from 'react';

const CountUpOnVisible = ({ targetNumber, duration = 2000, className = '' }) => {
  const countRef = useRef(null);
  const hasCountedRef = useRef(false);
  const [currentCount, setCurrentCount] = useState(0);

  useEffect(() => {
    const element = countRef.current;
    if (!element || hasCountedRef.current) return;

    const checkVisibilityAndCount = () => {
      const rect = element.getBoundingClientRect();
      // 判断元素是否进入视口(顶部小于窗口高度,底部大于0)
      const isVisible = rect.top < window.innerHeight && rect.bottom > 0;

      if (isVisible && !hasCountedRef.current) {
        hasCountedRef.current = true;
        let start = 0;
        const increment = targetNumber / (duration / 16); // 按16ms一帧计算增量

        const updateCount = () => {
          start += increment;
          if (start >= targetNumber) {
            setCurrentCount(targetNumber);
            return;
          }
          setCurrentCount(Math.floor(start));
          requestAnimationFrame(updateCount);
        };

        requestAnimationFrame(updateCount);
        // 计数开始后移除监听,避免重复触发
        window.removeEventListener('scroll', checkVisibilityAndCount);
        window.removeEventListener('resize', checkVisibilityAndCount);
      }
    };

    // 初始检查一次(元素可能一开始就在视口)
    checkVisibilityAndCount();
    // 添加滚动和窗口大小变化监听
    window.addEventListener('scroll', checkVisibilityAndCount);
    window.addEventListener('resize', checkVisibilityAndCount);

    // 清理函数
    return () => {
      window.removeEventListener('scroll', checkVisibilityAndCount);
      window.removeEventListener('resize', checkVisibilityAndCount);
    };
  }, [targetNumber, duration]);

  return (
    <span ref={countRef} className={className}>
      {currentCount}
    </span>
  );
};

// 使用示例
const App = () => {
  return (
    <div style={{ height: '150vh', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
      <div>
        累计用户数:<CountUpOnVisible targetNumber={12345} duration={2500} />
      </div>
    </div>
  );
};

export default App;

关键细节说明

  • 视口判断逻辑:通过getBoundingClientRect()获取元素的位置信息,结合window.innerHeight判断元素是否进入视口范围,确保只有元素可见时才启动计数。
  • 单次执行控制:用hasCountedRef标记是否已完成计数,初始为false,计数启动后设为true,后续不再触发逻辑;同时计数开始后立即移除滚动/resize监听,进一步避免重复执行。
  • 平滑计数动画:使用requestAnimationFrame实现流畅的帧动画,根据传入的duration计算每帧的增量,确保计数过程均匀平滑。
  • 通用性:组件通过props接收目标数值、动画时长、样式类名,可直接在任意需要的地方复用,无需针对特定场景修改核心逻辑。
  • 初始检查:挂载时先执行一次可见性检查,避免元素一开始就在视口但未触发计数的情况。

内容的提问来源于stack exchange,提问作者Duka93

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 07:10:07