如何重新启动因条件更新停止的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>会引发更多复杂问题。
解决方案
核心问题分析
- 原动画逻辑直接使用
requestAnimationFrame传入的全局时间戳(相对于页面加载),导致重启时无法重置动画计时; - 按钮事件绑定依赖DOM操作,且状态更新未正确触发动画的重新启动;
- 原
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
相关产品推荐
相关产品推荐

