React中requestAnimationFrame进度条动画无法被cancelAnimationFrame终止的原因
问题:React中requestAnimationFrame动画无法用cancelAnimationFrame停止
我在React中使用requestAnimationFrame实现了一个计时器进度条动画,但尽管为动画设置了ID,并在handleStop函数中引用该ID调用cancelAnimationFrame,却无法停止动画。这是为什么?
编辑补充:嗯,会不会是cancelAnimationFrame确实停止了requestAnimationFrame,但无法终止已经设置好的transform动画?
原代码
JavaScript
const { useState, useEffect } = React; const App = () => { const totalTime = 10000; const [timeLeft, setTimeLeft] = useState(null); const [timerId, setTimerId] = useState(null); useEffect(() => { setTimeLeft(totalTime); const start = performance.now(); const animate = (time) => { const elapsedTime = time - start; setTimeLeft((prevTimeLeft) => { if (prevTimeLeft !== null && prevTimeLeft > 0) { const remainingTime = Math.max(totalTime - elapsedTime, 0); requestAnimationFrame(animate); return remainingTime; } return 0; }); }; const id = requestAnimationFrame(animate); setTimerId(id); return () => { cancelAnimationFrame(id); }; }, []); const handleStop = () => { if (timerId) { cancelAnimationFrame(timerId); setTimerId(null); } }; return ( <div> <div className="progress-bar"> <div style={{ transform: `scaleX(${timeLeft ? timeLeft / totalTime : 0})`, }} /> </div> <button onClick={handleStop}>Pause the animation</button> </div> ); }; ReactDOM.render(<App />, document.getElementById("root"));
CSS
.progress-bar { height: 10px; width: 300px; background-color: #fdb913; border-radius: 10px; overflow: hidden; margin-top: 2rem; margin-bottom: 3rem; } .progress-bar div { background-color: grey; height: 100%; transform-origin: left center; }
HTML
<div id="root"> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script> </div>
问题原因
动画ID未跟踪最新值:你只保存了第一次调用
requestAnimationFrame返回的ID,但在animate函数内部,每次执行都会再次调用requestAnimationFrame(animate),生成新的动画ID,这些新ID从未被保存到timerId状态中。调用cancelAnimationFrame(timerId)只能终止第一次的动画帧,后续的动画帧仍在持续执行,导致动画停不下来。关于你补充的疑问:这里的
transform是通过React状态实时更新的内联样式,不是CSS关键帧动画,不存在“已设置的transform动画无法终止”的问题,动画继续运行的根源还是requestAnimationFrame的ID没跟踪到位。
修复方案
使用useRef替代useState来保存动画ID,因为useRef的更新不会触发组件重渲染,且能实时跟踪最新的动画ID:
修改后的JavaScript代码
const { useState, useEffect, useRef } = React; const App = () => { const totalTime = 10000; const [timeLeft, setTimeLeft] = useState(null); const animationIdRef = useRef(null); // 用ref保存最新动画ID useEffect(() => { setTimeLeft(totalTime); const start = performance.now(); const animate = (time) => { const elapsedTime = time - start; setTimeLeft((prevTimeLeft) => { if (prevTimeLeft !== null && prevTimeLeft > 0) { const remainingTime = Math.max(totalTime - elapsedTime, 0); // 保存新的动画ID到ref animationIdRef.current = requestAnimationFrame(animate); return remainingTime; } return 0; }); }; // 初始化动画并保存ID animationIdRef.current = requestAnimationFrame(animate); return () => { if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); } }; }, []); const handleStop = () => { if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = null; // 清空ID } }; return ( <div> <div className="progress-bar"> <div style={{ transform: `scaleX(${timeLeft ? timeLeft / totalTime : 0})`, }} /> </div> <button onClick={handleStop}>Pause the animation</button> </div> ); }; ReactDOM.render(<App />, document.getElementById("root"));
内容的提问来源于stack exchange,提问作者Adrian
相关产品推荐
相关产品推荐

