使用 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 中。
结论。大多数时候您可以认为这种副作用是无害的。
我将分两步介绍我的问题,两步的代码块略有不同。
第 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 中。
结论。大多数时候您可以认为这种副作用是无害的。