ReactJS 自定义useAnimationFrame Hook内如何获取更新后的状态变量
问题根因
你遇到的是典型的JS闭包捕获旧值问题:自定义Hook内部的useEffect仅在组件首次渲染时执行一次,绑定的是首次渲染时生成的动画回调,这个回调的闭包会固化当时能访问到的所有外部变量值,后续组件重渲染生成的新回调不会被Hook感知,因此读到的percentage永远是首次渲染时的初始值。
这也解释了为什么你的setProgress函数式更新逻辑能正常工作:它不依赖外部闭包捕获的变量,而是直接从React的状态队列读取最新的前值,不受旧闭包影响。
方案一:改造通用Hook,始终持有最新回调引用(推荐)
这是定时/动画类Hook解决闭包问题的通用标准实现,不需要改动业务侧的调用代码,改造后的Hook代码如下:
import React, { useRef, useEffect } from 'react'; const useAnimationFrame = (callback: (deltaTime: number) => void) => { const requestRef = useRef<number>(); const previousTimeRef = useRef<number>(); // 新增:存储每一次渲染生成的最新回调 const callbackRef = useRef(callback); // 每次组件重渲染后,自动把最新回调同步到ref,不触发额外重渲染 useEffect(() => { callbackRef.current = callback; }); const animate = (time: number) => { // 优化边界判断:避免时间戳为0时被误判为无值 if (previousTimeRef.current !== undefined) { const deltaTime = time - previousTimeRef.current; // 调用ref中存储的最新回调,而非首次绑定的旧回调 callbackRef.current(deltaTime); } previousTimeRef.current = time; requestRef.current = requestAnimationFrame(animate); }; useEffect(() => { requestRef.current = requestAnimationFrame(animate); return () => { if (requestRef.current) { cancelAnimationFrame(requestRef.current); } }; }, []); }; export default useAnimationFrame;
核心改动点:
- 新增
callbackRef字段,用来存储每一次渲染时传入的最新回调,避免旧闭包被永久绑定 - 新增无依赖的
useEffect,在组件每次重渲染后自动同步最新回调到callbackRef,整个过程基于ref操作,不会触发额外组件重渲染 - 帧执行逻辑中不再直接调用首次传入的原始回调,而是读取
callbackRef.current指向的最新回调,保证每帧拿到的闭包都是当前渲染周期的最新值,能正常访问更新后的percentage状态
方案二:业务侧手动同步状态到ref(无需改动通用Hook)
如果不想调整现有通用Hook的实现,可以沿用你现在处理exerciseRunning、directionForward的逻辑,把需要在动画回调里读取的状态同步到ref中,读取时直接取ref的当前值即可:
const [percentage, setPercentage] = useState(1); // 新增ref存储最新的percentage值 const percentageRef = useRef(percentage); // 监听percentage变化,实时同步到ref useEffect(() => { percentageRef.current = percentage; }, [percentage]); useAnimationFrame((deltaTime) => { if (!exerciseRunning.current) return; setProgress((prevState) => directionForward.current ? prevState + percentageRef.current * 1 : prevState - percentageRef.current * 2 ); });
两种方案的取舍:
- 方案一通用性更强,所有传入Hook的回调不管依赖了哪些状态,都能自动拿到最新值,不需要逐个手动同步,适合作为团队通用的动画工具Hook
- 方案二侵入性更低,适合不想改动现有通用Hook的场景,但如果后续有更多状态需要在动画回调里读取,每个都要手动加同步逻辑,维护成本更高
内容的提问来源于stack exchange,提问作者M.S.
相关产品推荐
相关产品推荐

