小能豆

React,setInterval 行为

javascript

let updateTimer: number;

export function Timer() {
  const [count, setCount] = React.useState<number>(0);
  const [messages, setMessages] = React.useState<string[]>([]);

  const start = () => {
    updateTimer = setInterval(() => {
      const m = [...messages];
      m.push("called");
      setMessages(m);
      setCount(count + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(updateTimer);
  };

  return (
    <>
      <div>{count}</div>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
      {messages.map((message, i) => (
        <p key={i}>{message}</p>
      ))}
    </>
  );
}

代码示例:https://codesandbox.io/s/romantic-wing-9yxw8 ?file=/src/App.tsx


代码有两个按钮——开始和停止。

  • 开始调用 asetInterval并保存间隔 id。计时器设置为 1 秒(1000 毫秒)。
  • clearIntervalStop在间隔 id 上调用 a 。

间隔 id 在组件外部声明。

间隔回调函数会增加计数器并将called消息附加到 UI。

当我单击“开始”时,我希望计数器每秒增加一次,并且called在按钮下方附加相应的消息。

实际情况是,单击“开始”时,计数器仅增加一次,called消息也仅增加一次。

如果我再次单击“开始”,计数器将递增,然后重置回其先前的值。

如果我继续单击“开始”,计数器将不断递增并重置回其先前的值。

有人能解释这种行为吗?


阅读 76

收藏
2024-06-16

共1个答案

小能豆

您在间隔的回调中对值进行了闭包。因此,在使用值进行第一次状态更新后,您将拥有相同的值调用,不会触发另一个渲染。count``setState(0+1)``count``setState(0+1)

使用不使用闭包的先前状态值的功能更新:

setCount((count) => count + 1);

原因相同messages

setMessages(prev => [...prev,"called"]);
const start = () => {
  // should be a ref
  intervalId.current = setInterval(() => {
    setMessages((prev) => [...prev, "called"]);
    setCount((count) => count + 1);
  }, 1000);
};

注意 使用外部范围变量而不是的另一个可能的错误useRef,有关此错误,请阅读有关useRefvs 变量差异的信息。


作为参考,这里有一个简单的计数器切换示例:

function Component() {
  // use ref for consisent across multiple components
  // see https://stackoverflow.com/questions/57444154/why-need-useref-to-contain-mutable-variable-but-not-define-variable-outside-the/57444430#57444430
  const intervalRef = useRef();

  const [counter, setCounter] = useState(0);

  // simple toggle with reducer
  const [isCounterOn, toggleCounter] = useReducer((p) => !p, false);

  // handle toggle
  useEffect(() => {
    if (isCounterOn) {
      intervalRef.current = setInterval(() => {
        setCounter((prev) => prev + 1);
      }, 1000);
    } else {
      clearInterval(intervalRef.current);
    }
  }, [isCounterOn]);

  // handle unmount
  useEffect(() => {
    // move ref value into callback scope
    // to not lose its value upon unmounting
    const intervalId = intervalRef.current;
    return () => {
      // using clearInterval(intervalRef.current) may lead to error/warning
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      {counter}
      <button onClick={toggleCounter}>Toggle</button>
    </>
  );
}
2024-06-16