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

如何在ReactJS中不使用外部包实现里程表样式过渡效果?

纯React实现无依赖里程表(Odometer)效果

基础计数动画版本

这个版本实现数字从初始值平滑过渡到目标值的效果,核心用React内置hooks结合requestAnimationFrame实现流畅动画:

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

const Odometer = ({ targetValue, duration = 2000 }) => {
  const [currentValue, setCurrentValue] = useState(0);
  const startValueRef = useRef(0);
  const startTimeRef = useRef(null);

  useEffect(() => {
    startValueRef.current = currentValue;
    startTimeRef.current = Date.now();

    const updateValue = () => {
      const elapsed = Date.now() - startTimeRef.current;
      const progress = Math.min(elapsed / duration, 1);
      // 用easeOutQuad缓动函数让动画更自然
      const easedProgress = 1 - Math.pow(1 - progress, 2);
      const newValue = Math.floor(startValueRef.current + (targetValue - startValueRef.current) * easedProgress);
      
      setCurrentValue(newValue);

      if (progress < 1) {
        requestAnimationFrame(updateValue);
      }
    };

    requestAnimationFrame(updateValue);
  }, [targetValue]);

  return <div className="odometer-value">{currentValue}</div>;
};

// 使用示例
const App = () => {
  return <Odometer targetValue={100} />;
};

export default App;

进阶:逐位滚动的真实里程表效果

如果要模拟真实里程表的逐位数字滚动,可将数字拆分为单独数位,每个数位独立做滚动动画:

import { useState, useEffect, useRef } from 'react';
import './Odometer.css';

const Digit = ({ targetDigit, duration = 2000 }) => {
  const [currentDigit, setCurrentDigit] = useState(0);
  const startDigitRef = useRef(0);
  const startTimeRef = useRef(null);

  useEffect(() => {
    startDigitRef.current = currentDigit;
    startTimeRef.current = Date.now();

    const updateDigit = () => {
      const elapsed = Date.now() - startTimeRef.current;
      const progress = Math.min(elapsed / duration, 1);
      const easedProgress = 1 - Math.pow(1 - progress, 2);
      // 处理跨0的滚动逻辑(比如9→0)
      let diff = targetDigit - startDigitRef.current;
      if (diff < 0) diff += 10;
      const newDigit = (startDigitRef.current + Math.floor(diff * easedProgress)) % 10;
      
      setCurrentDigit(newDigit);

      if (progress < 1) {
        requestAnimationFrame(updateDigit);
      }
    };

    requestAnimationFrame(updateDigit);
  }, [targetDigit]);

  return <div className="odometer-digit">{currentDigit}</div>;
};

const Odometer = ({ targetValue, duration = 2000 }) => {
  // 将目标值转换为补位后的数位数组,比如100转为[1,0,0]
  const digits = targetValue.toString().split('').map(Number);

  return (
    <div className="odometer-value">
      {digits.map((digit, index) => (
        <Digit key={index} targetDigit={digit} duration={duration} />
      ))}
    </div>
  );
};

// Odometer.css 样式示例
/*
.odometer-value {
  display: flex;
  gap: 4px;
  font-family: monospace;
  font-size: 24px;
}

.odometer-digit {
  width: 24px;
  text-align: center;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 4px;
}
*/

// 使用示例
const App = () => {
  return <Odometer targetValue={100} />;
};

export default App;

核心说明

  • 用requestAnimationFrame替代setInterval,避免掉帧,保证动画流畅度
  • 加入缓动函数让数字过渡更自然,而非生硬的线性变化
  • 进阶版本处理了数字跨0的滚动逻辑,更贴近真实里程表体验
  • 所有实现仅依赖React内置hooks,无任何第三方包

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 17:42:32