You need to enable JavaScript to run this app. ReactJS 自定义useAnimationFrame Hook内如何获取更新后的状态变量
优惠活动
大模型
产品
解决方案
定价
更多

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.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.30 07:36:09