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

如何重新启动因条件更新停止的React Hooks逐帧动画?

React像素艺术动画重启问题

我正在开发一个初学者React项目,实现像素艺术动画效果:通过遍历数组切换猫咪图像。核心逻辑是用requestAnimationFrame递归调用animate函数,传入时间戳驱动动画:

  • setSource利用当前与历史时间戳的差值iteration,结合取模运算将source限制在[0,1],切换images数组的第0、1帧;
  • 当时间戳达到5000ms时,动画停止,切换到images[2]。

现在需要实现点击按钮重启整个递归动画循环,但尝试的方法没达到预期效果。


现有代码

import { useRef, useState, useEffect } from "react";
import React from "react"

// 注:原代码缺少export,此处保留用户原始写法
default function placeHolder() {
  const [source, setSource] = useState(0);
  const temp = React.useRef();
  const images = ['gray-pixil-frame-1.png', 'gray-pixil-frame-0.png', 'gray-pixil-frame-2.png']
  var result = images[Math.round(source)]

  const animate = timer=> {
      console.log(timer)
      if (timer <= 5000) {
        if (temp.current != undefined) {
          const iteration = timer - temp.current;
          setSource(
            (prevCount =>
              (prevCount + iteration * 0.002) % 1)
          )
        }
        temp.current = timer;
        requestAnimationFrame(animate);
      }
      else if (timer > 5000) {
        setSource(2) // 时间到5000ms后切换到第三帧
      }
  }

  React.useEffect(() => {
    const id = requestAnimationFrame(animate)    
    return () => cancelAnimationFrame(id)
  },[])

  return (
      <img key={result} src={`image/${result}`} className="img" />
  );
}

已尝试的方法:布尔值与useEffect

通过useState初始化布尔值isAnimationActive,点击按钮时更新状态尝试控制动画,但效果未达预期。后续通过useEffect绑定按钮DOM事件,尝试重置动画状态并切换布尔值,仍失败。

// 补充:用户代码中应先声明isAnimationActive状态
const [isAnimationActive, setIsAnimationActive] = useState(true);

useEffect(() => {
  const handleButtonClick = () => {
    if (isAnimationActive) {
      setSource(0); // 重置动画source
      temp.current = undefined; // 重置temp
    }
    setIsAnimationActive(!isAnimationActive); // 切换动画状态
  };
  const questionsButton = document.getElementById("questions");
  questionsButton.addEventListener('click', handleButtonClick);
  return () => {
    questionsButton.removeEventListener('click', handleButtonClick);
  };
}, []);

注:使用document.getElementById()绑定按钮事件,因为直接用<button onClick>会引发更多复杂问题。


解决方案

核心问题分析

  1. 原动画逻辑直接使用requestAnimationFrame传入的全局时间戳(相对于页面加载),导致重启时无法重置动画计时;
  2. 按钮事件绑定依赖DOM操作,且状态更新未正确触发动画的重新启动;
  3. 原useEffect的依赖数组为空,仅在组件挂载时执行一次,无法响应按钮点击的状态变化。

具体修改步骤

1. 新增动画控制状态

用animationTrigger状态作为动画重启的开关,每次点击按钮时更新它,触发useEffect重新执行动画:

const [animationTrigger, setAnimationTrigger] = useState(0);

2. 修复动画计时逻辑

记录动画启动时的时间戳,计算相对运行时间,避免依赖全局时间戳:

const animate = (startTime) => {
  return (currentTime) => {
    const elapsed = currentTime - startTime;
    if (elapsed <= 5000) {
      if (temp.current !== undefined) {
        const iteration = currentTime - temp.current;
        setSource(prev => (prev + iteration * 0.002) % 1);
      }
      temp.current = currentTime;
      requestAnimationFrame(animate(startTime));
    } else {
      setSource(2);
    }
  };
};

3. 绑定按钮点击事件(React原生方式)

改用React的onClick配合状态控制,避免DOM操作带来的潜在问题:

const handleRestart = () => {
  // 重置动画状态
  setSource(0);
  temp.current = undefined;
  // 更新触发状态,让useEffect重新启动动画
  setAnimationTrigger(prev => prev + 1);
};

// 在返回的JSX中添加按钮
return (
  <div>
    <img key={result} src={`image/${result}`} className="img" />
    <button id="questions" onClick={handleRestart}>重启动画</button>
  </div>
);

4. 更新useEffect依赖

让useEffect依赖animationTrigger,每次触发状态变化时重启动画:

useEffect(() => {
  const startTime = performance.now();
  temp.current = startTime;
  const animationId = requestAnimationFrame(animate(startTime));
  
  return () => cancelAnimationFrame(animationId);
}, [animationTrigger]);

完整修改后的代码

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

export default function placeHolder() {
  const [source, setSource] = useState(0);
  const [animationTrigger, setAnimationTrigger] = useState(0);
  const temp = useRef();
  const images = ['gray-pixil-frame-1.png', 'gray-pixil-frame-0.png', 'gray-pixil-frame-2.png'];
  const result = images[Math.round(source)];

  // 重构animate函数,基于启动时间计算相对时长
  const animate = (startTime) => {
    return (currentTime) => {
      const elapsed = currentTime - startTime;
      if (elapsed <= 5000) {
        if (temp.current !== undefined) {
          const iteration = currentTime - temp.current;
          setSource(prev => (prev + iteration * 0.002) % 1);
        }
        temp.current = currentTime;
        requestAnimationFrame(animate(startTime));
      } else {
        setSource(2);
      }
    };
  };

  useEffect(() => {
    const startTime = performance.now();
    temp.current = startTime;
    const animationId = requestAnimationFrame(animate(startTime));
    
    return () => cancelAnimationFrame(animationId);
  }, [animationTrigger]);

  const handleRestart = () => {
    // 重置动画状态
    setSource(0);
    temp.current = undefined;
    // 触发动画重启
    setAnimationTrigger(prev => prev + 1);
  };

  return (
    <div>
      <img key={result} src={`image/${result}`} className="img" />
      <button id="questions" onClick={handleRestart}>重启动画</button>
    </div>
  );
}

关键说明

  • 用animationTrigger状态作为动画重启的开关,每次点击按钮更新它,让useEffect重新执行并启动新的动画循环;
  • 动画计时改用相对时间(elapsed = currentTime - startTime),避免原逻辑中直接使用全局时间戳导致重启计时混乱的问题;
  • 移除DOM事件绑定,改用React原生的onClick,符合React状态驱动的编程模式,避免DOM操作带来的潜在问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 17:44:51