使用 setInterval 时,React 渲染时不会实际更改状态

React renders without actually changing the state when setInterval is used

我将分两步介绍我的问题,两步的代码块略有不同。

第 1 步:

下面我们有一个 React 应用程序,它每两秒呈现一次,因此导致浏览器将 render 打印到控制台。如果用户按下任意键,渲染将停止,从而停止控制台打印。请暂时忽略注释掉的行。

import { useState, useEffect, useRef } from 'react';

function App() {

  const [number, setUpdate] = useState(0);
  const [isPaused, setIsPaused] = useState(false);

  const intervalRef = useRef(undefined);

  useEffect(() => {
    intervalRef.current = setInterval(() => setUpdate(prevNumber => ++prevNumber), 2000);
    window.addEventListener('keydown', handleKeyDown);
  }, []);

  const handleKeyDown = () => {
    clearInterval(intervalRef.current);
    console.log('console log here');

    // setIsPaused(isPaused);
  };

  console.log('render');

  return null;
};

export default App;

这是应用程序的屏幕截图:

上面发生的事情是,我让组件渲染了五次,然后我按下了一个键来停止组件渲染。

第 2 步:

在第 2 步中,我们有完全相同的应用程序,除了 未注释掉 handleKeyDown.[=14 中设置的状态=]

  const handleKeyDown = () => {
    clearInterval(intervalRef.current);
    console.log('console log here');

    // This is no longer commented out. Why does it cause a new render?
    setIsPaused(isPaused);
  };

这是在第 2 步中进行了代码更改的应用程序的屏幕截图:

同样,我让组件在按下一个键后渲染五次。但是现在有一个额外的渲染,即使状态不应该改变(因为状态实际上并没有发生变化,因为我们设置了与状态中已经存在的相同的值)setIsPaused(isPaused).

我很难理解在第 2 步导致额外渲染的可能原因。也许这是显而易见的事情?

setIsPaused(isPaused) 如果我注释掉 setInterval 的 运行 其他状态更改,则永远不会导致新渲染,这让我更加困惑。

U̶p̶d̶a̶t̶i̶n̶g̶ ̶a̶ ̶s̶t̶a̶t̶e̶ ̶n̶e̶v̶e̶r̶ ̶m̶e̶a̶n̶s̶ ̶D̶O̶M̶ ̶w̶i̶l̶l̶ ̶r̶e̶r̶e̶n̶d̶e̶r̶,̶ ̶y̶o̶u̶ ̶a̶r̶e̶ ̶r̶i̶g̶h̶t̶ ̶̶s̶e̶t̶I̶s̶P̶a̶u̶s̶e̶d̶(̶i̶s̶P̶a̶u̶s̶e̶d̶)̶̶ ̶w̶i̶l̶l̶ ̶n̶o̶t̶ ̶r̶e̶r̶e̶n̶d̶e̶r̶ ̶t̶h̶e̶ ̶D̶O̶M̶,̶ ̶b̶u̶t̶ ̶i̶t̶ ̶w̶i̶l̶l̶ ̶s̶e̶t̶ ̶t̶h̶e̶ ̶s̶t̶a̶t̶e̶ ̶a̶n̶d̶ ̶w̶i̶l̶l̶ ̶g̶i̶v̶e̶ ̶y̶o̶u̶ ̶t̶h̶e̶ ̶u̶p̶d̶a̶t̶e̶d̶ ̶v̶a̶l̶u̶e̶.̶ ̶(̶I̶ ̶a̶m̶m̶c̶o̶n̶s̶i̶d̶e̶e̶e̶i̶n̶g̶g̶̶

更新:在阅读已接受的答案之前,我确实知道存在这种行为。

这是一个已知的怪癖,请参阅 #17474。这是新的并发模式引入的副作用。组件函数 re-run,但 DOM 将保持不变。

我也找人postthis interesting example。你可以用代码试试 exp。组件函数包含类似 <div>{Math.random()}</div> 的内容,尽管随机数在函数的额外 re-run 中发生了变化,但如果状态未更改,它不会反映到 DOM 中。

结论。大多数时候您可以认为这种副作用是无害的。