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

如何让CSS动画与React倒计时效果保持同步?

问题描述

我正在开发一个React组件,实现圆形倒计时进度条搭配周期性文本倒计时:圆形进度条30秒完成一次倒计时循环,文本显示5-4-3-2-1-0循环往复。但当前文本与CSS动画不同步,圆形计时器会在文本循环中途结束。

CSS代码

.animation {
  animation: countdown 30s linear infinite forwards;
}

React组件代码

const [reset, setReset] = useState(props.reset);
const [period, setPeriod] = useState(props.period);
const [seconds, setSeconds] = useState(reset);
const [runTime, setRunTime] = useState(0);

const circle = useRef<SVGCircleElement>(null);

useLayoutEffect(() => {
  circle.current?.classList.add(css.animation);

  const interval = setInterval(() => {
    setSeconds((seconds) => {
      if (seconds > 0) {
        return seconds - 1;
      } else {
        return reset;
      }
    });
  }, 1000);

  // Check total time
  setRunTime((runTime) => runTime + 1);
  if (runTime === period * 60) {
    clearInterval(this);
    setFeedback('Complete');
    circle.current?.classList.remove(css.animation);
  }

  return () => clearInterval(interval);
}, [seconds]);

return (
  <>
    <svg className={css.svg}>
      <circle ref={circle} r="18" cx="20" cy="20"></circle>
    </svg>
    <p id={css.countdownNumber}>{seconds}</p>
  </>
);

请问useLayoutEffect中的计算是否会导致循环延迟?该如何让两个计时器保持同步?


问题解答

一、useLayoutEffect中的问题分析

当前实现确实会导致循环延迟和不同步,核心问题集中在以下几点:

  • 依赖数组错误:依赖seconds意味着每次文本更新都会重新执行useLayoutEffect,频繁创建并销毁setInterval,不仅会累积时间误差,还会反复重启CSS动画,直接打断动画连续性。
  • runTime逻辑无效:setRunTime调用后,闭包捕获的runTime仍是旧值,后续的周期判断永远无法触发;且clearInterval(this)是错误写法,this并非interval引用。
  • 双计时器独立运行:CSS动画与JSsetInterval是两个独立计时系统,setInterval本身受事件循环阻塞影响存在精度问题,再加上频繁重建,必然导致两者不同步。

二、同步解决方案:单一时间源驱动

要实现完美同步,必须用同一个时间基准驱动文本和进度条,推荐通过计算起始时间与当前时间的差值来同步所有状态,避免依赖多个独立计时器。

优化后的代码示例

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

const Countdown = ({ reset = 5, period = 1 }) => {
  const [seconds, setSeconds] = useState(reset);
  const [isComplete, setIsComplete] = useState(false);
  const circleRef = useRef<SVGCircleElement>(null);
  const startTimeRef = useRef<number>(Date.now());
  const animationFrameRef = useRef<number>(0);

  useEffect(() => {
    if (isComplete) return;

    const updateCountdown = () => {
      const elapsed = Date.now() - startTimeRef.current;
      const totalCycleMs = 30 * 1000; // CSS动画周期30秒
      const textCycleMs = (reset + 1) * 1000; // 文本循环周期:5-0共6秒

      // 计算当前文本显示的秒数
      const textElapsed = elapsed % textCycleMs;
      const currentSecond = reset - Math.floor(textElapsed / 1000);
      setSeconds(currentSecond);

      // 检查是否到达总周期(period分钟)
      if (elapsed >= period * 60 * 1000) {
        setIsComplete(true);
        circleRef.current?.classList.remove('animation');
        return;
      }

      animationFrameRef.current = requestAnimationFrame(updateCountdown);
    };

    // 启动CSS动画和循环更新
    circleRef.current?.classList.add('animation');
    animationFrameRef.current = requestAnimationFrame(updateCountdown);

    // 清理函数
    return () => {
      cancelAnimationFrame(animationFrameRef.current);
      circleRef.current?.classList.remove('animation');
    };
  }, [reset, period, isComplete]);

  return (
    <>
      <svg className="svg">
        <circle ref={circleRef} r="18" cx="20" cy="20"></circle>
      </svg>
      <p className="countdownNumber">{seconds}</p>
    </>
  );
};

export default Countdown;

关键优化点

  1. 单一时间源:用Date.now()记录起始时间,通过流逝时间计算状态,完全规避setInterval的精度问题。
  2. 避免重复初始化:useEffect仅在依赖项变化时执行一次,不会因文本更新重复创建计时器,保证动画和状态更新的连续性。
  3. 精确周期匹配:文本循环周期(6秒)是CSS动画周期(30秒)的约数,天然保证循环同步。
  4. 正确清理逻辑:用requestAnimationFrame的返回值取消循环,避免内存泄漏。

额外补充:JS直接控制进度条

如果需要100%同步,可以移除CSS动画,直接通过JS计算进度更新圆形的stroke-dasharray和stroke-dashoffset:

// 在updateCountdown函数中添加:
const progress = (elapsed % totalCycleMs) / totalCycleMs;
if (circleRef.current) {
  const circumference = 2 * Math.PI * 18; // r=18的圆周长
  circleRef.current.style.strokeDasharray = `${circumference}`;
  circleRef.current.style.strokeDashoffset = `${circumference * progress}`;
}

此时可删除CSS中的.animation类,完全由JS控制进度,彻底消除同步误差。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 10:05:37