反应 setInterval 行为

Posted

技术标签:

【中文标题】反应 setInterval 行为【英文标题】:React setInterval Behavior 【发布时间】:2021-04-05 21:53:39 【问题描述】:
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


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

Start 调用 setInterval 并保存间隔 id。计时器设置为 1 秒(1000 毫秒)。

Stop 在间隔 id 上调用 clearInterval

区间id在组件外声明。

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

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

实际发生的情况是,在单击“开始”时,计数器只增加一次,called 消息也是如此。

如果我再次单击“开始”,计数器会增加并随后重置为之前的值。

如果我继续点击“开始”,计数器会继续递增并重置为之前的值。

谁能解释这种行为?

【问题讨论】:

【参考方案1】:

您在间隔回调中的count 值上有closure。

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

使用functional updates,它使用之前没有闭包的状态值:

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,为此请阅读 useRef vs variable differences。


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

function Component() 
  // use ref for consisent across multiple components
  // see https://***.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>
    </>
  );

【讨论】:

啊,比我快 7 秒。我建议推动使用 react ref 并使用 useEffect 回调返回函数来清除卸载时的间隔,因此不会出现“无法更新卸载的 XXX”错误。 同意,应该将setInterval() 包裹在useEffect() 中。通过isCounting等状态触发效果,点击开始和停止按钮时设置isCounting

以上是关于反应 setInterval 行为的主要内容,如果未能解决你的问题,请参考以下文章

AS2.0 setInterval 和 clearInterval的用法

setInterval方法怎么了?

setInterval的使用和停用

在 setInterval 中调用 clearInterval() 不会停止 setinterval

第一次立即执行 setInterval 函数

为啥 requestAnimationFrame 比 setInterval 或 setTimeout 更好