【问题标题】:Cancelling requestAnimationRequest in a React component using hooks doesn't work使用钩子取消 React 组件中的 requestAnimationRequest 不起作用
【发布时间】:2020-06-30 08:17:32
【问题描述】:

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

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

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

【问题讨论】:

    标签: javascript reactjs frontend react-hooks requestanimationframe


    【解决方案1】:

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

    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>

    无论如何,您更新状态的次数也比实际需要的多得多,您可以通过使用refsrequestAnimationFrame 提供的时间戳 (time) 来跟踪当前和下一个状态,从而避免这种情况/target 计数器值。您仍将调用 requestAnimationFrame 更新函数相同的次数,但只有在您知道更改将反映在 UI 中时,您才会更新状态 (setCount(...))。

    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>

    【讨论】:

    • 嘿,谢谢!我想我快到了。 @丹齐格。您可以在动画中添加重置按钮吗? (在计数器组件之外的某个地方,它接受一个要重置的道具。)我的应用程序基本上是一个进度条(这里是计数器),范围从 0-> 100,它需要在路由器更改时重置为 0 并返回到 100。正在将路由器更改标志传递给进度条(此处为计数器)。我猜按钮重置应该是类似的。这就是我首先添加 cancelAnimationFrame 的原因。
    • 如果是对的,我可能需要在返回逻辑上设置 targetValueRef.current = 0 cancelAnimationFrame(requestRef.current)。
    • codesandbox.io/s/styled-components-react-playground-e3osc 如果你看到这里,我的进度条工作了。虽然,不知道如何让路由器参与这个游乐场。
    • @NevinMadhukarK 我已经更新了代码。每当某些相关属性发生变化时,您只需要使用useEffect 来重置计数器。在这种情况下,我只是将位置(假的)传递给Counter 组件。
    猜你喜欢
    • 2020-03-29
    • 2020-12-22
    • 2020-12-21
    • 2020-10-18
    • 2020-12-11
    • 2020-03-11
    • 2021-11-13
    • 1970-01-01
    • 2021-10-26
    相关资源
    最近更新 更多