在React中使用requestAnimationFrame

7

我正在阅读这篇文章,但不确定最终的hook是如何工作的。

以下是代码:

const useAnimationFrame = (callback) => {
  const requestRef = useRef();
  const previousTimeRef = useRef();

  const animate = (time) => {
    if (previousTimeRef.current !== undefined) {
      const deltaTime = time - previousTimeRef.current;
      callback(deltaTime);
    }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  };

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);
}

并且可以以以下方式进行使用:

const [count, setCount] = useState(0);

useAnimationFrame((deltaTime) => {
  setCount((prevCount) => {
    return prevCount + 1;
  });
});

好的,目标是每帧递增一个数字值。

我可以解释一下运行此代码时发生了什么:

  1. the component create a local state with useState(0)

  2. then the useAnimationFrame hook is called using this callback as parameter:

    (deltaTime) => {
      setCount((prevCount) => {
        return prevCount + 1;
      });
    }
    

该函数接受一个数字作为输入,并在每次调用时将状态值增加一。

  1. useAnimationFrame 是一个接受另一个函数(回调函数)作为参数的函数。它创建了两个引用。第一次执行时(由于 []),它调用了 useEffect。它把 requestAnimationFrame 返回的时间戳保存在 requestRef.current 中。 requestRef.current 调用 animate 函数,计算请求动画帧之间的差值,然后使用该值调用回调函数,从而调用 setCount。然后它更新当前引用的值并再次调用 requestAnimationFrame

因此,循环应该是:

component 
  > count = 0
useAnimationFrame             <--------------+
  > requestRef = ?                           |
  > previousTimeRef = ?                      |
    useEffect                                |
      animate                                |
        > deltaTime = delta#1                |
        > count = 1                          |
        > previousTimeRef.current = time#1   |
        > requestRef.current = time#2 -------+
      > requestRef.current = timestamp#1

我错了吗?


useEffect 应该依赖于回调函数。如果您的回调函数依赖于其他钩子中的外部值,则不会被更新。 - Coco
1个回答

4

了解 requestAnimationFramecancelAnimationFrame 的函数签名可能会有所帮助。

requestAnimationFrame 接收一个回调函数作为单个参数。回调函数本身接收一个时间戳参数(DOMHighResTimeStamp)。

cancelAnimationFrame 接收一个参数,即要取消的 requestAnimationFrameid

因此,在 animate 回调函数中,time 是通过 API 接收的单个参数,它是类似于 performance.now() 返回的 DOMHighResTimeStamp,表示 requestAnimationFrame() 开始执行回调函数时的时间点。

 const animate = (time) => {

这是一个检查钩子是否已经运行了1次的过程。如果是,则使用新时间减去上一次时间更新父级React范围。
    if (previousTimeRef.current !== undefined) {
      const deltaTime = time - previousTimeRef.current;
      callback(deltaTime);
    }

确认hook已运行后,保存DOMHighResTimeStamp用于未来的计算。

    previousTimeRef.current = time;

接下来,这里有一些有趣的部分,我不确定这是最佳方法。它甚至可能是一个错误。该代码设置了一个新的监听器,并根据新调用的结果更新 ref 中的最新id。

仅从阅读代码中,我不确定原始监听器是否被取消。我怀疑它没有。

    /// this is an id
    requestRef.current = requestAnimationFrame(animate);

我无法访问正在运行的版本,但我建议完全删除requestRef.current,看看当useEffect清理发生时是否按预期进行了清理,例如:

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

这也将简化嵌入的refs,使阅读更加清晰。


requestRef.current 在每次循环中都会更新,因此无论何时 useEffect 运行其清除函数,它都会取消当前的 requestAnimationFrame 而不是第一个创建的。 - Sheraff
requestAnimationFrame 在 JS 动画中是一种标准实践,它会在循环中调用自身。 - Sheraff
也许更好的说法是 - 这段代码中的 useEffect 不会在每个 requestAnimationFrame 上运行/重新渲染。虽然没有重新渲染,但 requestAnimationFrame 循环将继续迭代。当它重新渲染时,清除函数将注销 id 并初始化新的动画帧循环。因此,requestRef.current 是多余的并且增加了不必要的复杂性。 - nrako
1
我很想看到一个工作的概念证明,因为它不符合我的心理模型。每次调用requestAnimationFrame都会生成一个新的 id。在一堆迭代之后使用第一个 id 取消将没有任何效果。 - Sheraff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接