使用钩子在React组件中取消requestAnimationRequest不起作用 [英] Cancelling requestAnimationRequest in a React component using hooks doesn't work

查看:98
本文介绍了使用钩子在React组件中取消requestAnimationRequest不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在进度条上(最终是..),我想在达到某个值(10、100,...,N)时停止动画(调用cancelAnimationRequest)并将其重置为0.

I am working on a progress bar (Eventually..) and I want to stop the animation (calling cancelAnimationRequest) when reaching a certain value (10, 100, ..., N) and reset it to 0.

但是,使用我当前的代码,它会重置为0,但会无限期地运行.我认为这部分代码可能有问题:

However, with my current code, it resets to 0 but keeps running indefinitely. I think I might have something wrong in this part of the code:

setCount((prevCount) => {
    console.log('requestRef.current', requestRef.current, prevCount);
    
    if (prevCount < 10) return prevCount + deltaTime * 0.001;
    
    // Trying to cancel the animation here and reset to 0:
    cancelAnimationFrame(requestRef.current);

    return 0;
});

这是整个示例:

const Counter = () => {
  const [count, setCount] = React.useState(0);

  // Use useRef for mutable variables that we want to persist
  // without triggering a re-render on their change:
  const requestRef = React.useRef();
  const previousTimeRef = React.useRef();

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

      // Pass on a function to the setter of the state
      // to make sure we always have the latest state:
      setCount((prevCount) => {
        console.log('requestRef.current', requestRef.current, prevCount);
        
        if (prevCount < 10) return prevCount + deltaTime * 0.001;
        
        // Trying to cancel the animation here and reset to 0:
        cancelAnimationFrame(requestRef.current);

        return 0;
      });
    }
    
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  }

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

  return <div>{ Math.round(count) }</div>;
}

ReactDOM.render(<Counter />, document.getElementById('app'));

html {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

body {
  font-size: 60px;
  font-weight: 700;
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background-color: #A3E3ED;
}

.as-console-wrapper {
  max-height: 66px !important;
}

<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

手写笔: https://codepen.io/fr-nevin/pen/RwrLmPd

推荐答案

您的代码的主要问题是您试图取消已经执行的更新.相反,您可以避免请求不需要的最后一次更新.您可以在下面看到问题和简单的解决方案:

The main problem with your code is that you are trying to cancel an update that has already been executed. Instead, you can just avoid requesting that last update that you don't need. You can see the problem and a simple solution for that below:

const Counter = () => {
  const [count, setCount] = React.useState(0);
  const requestRef = React.useRef();
  const previousTimeRef = React.useRef(0);

  const animate = React.useCallback((time) => {
    console.log('       RUN:', requestRef.current);

    setCount((prevCount) => {  
      const deltaTime = time - previousTimeRef.current;   
      const nextCount = prevCount + deltaTime * 0.001;
      
      // We add 1 to the limit value to make sure the last valid value is
      // also displayed for one whole "frame":
      if (nextCount >= 11) {
        console.log('    CANCEL:', requestRef.current, '(this won\'t work as inteneded)');
        
        // This won't work:
        // cancelAnimationFrame(requestRef.current);
        
        // Instead, let's use this Ref to avoid calling `requestAnimationFrame` again:
        requestRef.current = null;
      }
      
      return nextCount >= 11 ? 0 : nextCount;
    });
    
    // If we have already reached the limit value, don't call `requestAnimationFrame` again:
    if (requestRef.current !== null) {
      previousTimeRef.current = time;
      requestRef.current = requestAnimationFrame(animate);
      console.log('- SCHEDULE:', requestRef.current);
    }     
  }, []);

  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);
  
  // This floors the value:
  // See https://stackoverflow.com/questions/7487977/using-bitwise-or-0-to-floor-a-number.
  return (<div>{ count | 0 } / 10</div>);
};

ReactDOM.render(<Counter />, document.getElementById('app'));

html {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

body {
  font-size: 60px;
  font-weight: 700;
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background-color: #A3E3ED;
}

.as-console-wrapper {
  max-height: 66px !important;
}

<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

在任何情况下,您更新状态的次数都比实际需要的次数多,这可以通过使用参考requestAnimationFrame提供的时间戳(time),以跟踪当前和下一个/目标计数器值.您仍将多次调用requestAnimationFrame更新函数,但是只有在知道更改将反映在UI中后,才更新状态(setCount(...)).

In any case, you are also updating the state many more times than actually needed, which you can avoid by using refs and the timestamp (time) provided by requestAnimationFrame to keep track of the current and next/target counter values. You are still going to call the requestAnimationFrame update function the same number of times, but you will only update the state (setCount(...)) once you know the change is going to be reflected in the UI.

const Counter = ({ max = 10, rate = 0.001, location }) => {
  const limit = max + 1;
  const [count, setCount] = React.useState(0);
  const t0Ref = React.useRef(Date.now());
  const requestRef = React.useRef();
  const targetValueRef = React.useRef(1);

  const animate = React.useCallback(() => {
    // No need to keep track of the previous time, store initial time instead. Note we can't 
    // use the time param provided by requestAnimationFrame to the callback, as that one won't
    // be reset when the `location` changes:
    const time = Date.now() - t0Ref.current;    
    const nextValue = time * rate;
    
    if (nextValue >= limit) {
      console.log('Reset to 0');
      setCount(0);
      return;
    }
    
    const targetValue = targetValueRef.current;      
    
    if (nextValue >= targetValue) {
      console.log(`Update ${ targetValue - 1 } -> ${ nextValue | 0 }`);      
      setCount(targetValue);
      targetValueRef.current = targetValue + 1;
    }
    
    requestRef.current = requestAnimationFrame(animate);
  }, []);
  
  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    
    return () => cancelAnimationFrame(requestRef.current);
  }, []);

  React.useEffect(() => {
    // Reset counter if `location` changes, but there's no need to call `cancelAnimationFrame` .
    setCount(0);
    t0Ref.current = Date.now();
    targetValueRef.current = 1;    
  }, [location]);
  
  return (<div className="counter">{ count } / { max }</div>);
};

const App = () => {
  const [fakeLocation, setFakeLocation] = React.useState('/');
  
  const handleButtonClicked = React.useCallback(() => {
    setFakeLocation(`/${ Math.random().toString(36).slice(2) }`);
  }, []);

  return (<div>  
    <span className="location">Fake Location: { fakeLocation }</span>
    <Counter max={ 10 } location={ fakeLocation } />
    <button className="button" onClick={ handleButtonClicked }>Update Parent</button>
  </div>);
};

ReactDOM.render(<App />, document.getElementById('app'));

html {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

body {
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background-color: #A3E3ED;
}

.location {
  font-size: 16px;
}

.counter {
  font-size: 60px;
  font-weight: 700;
}

.button {
  border: 2px solid #5D9199;
  padding: 8px;
  margin: 0;
  font-family: 'Roboto Mono', monospace;
  color: #5D9199;
  background: transparent;
  outline: none;
}

.as-console-wrapper {
  max-height: 66px !important;
}

<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

这篇关于使用钩子在React组件中取消requestAnimationRequest不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆