使用钩子取消 React 组件中的 requestAnimationRequest 不起作用

Cancelling requestAnimationRequest in a React component using hooks doesn't work

我正在制作进度条(最终..),我想在达到某个值(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

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

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 
  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) 来避免这种情况跟踪当前和 next/target 计数器值。您仍将调用 requestAnimationFrame 更新函数相同的次数,但是您只会更新状态 (setCount(...)) 一旦您知道更改将反映在 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>