我正在制作一个简单的进度条,当我使用 setInterval 时,我注意到钩子状态的奇怪行为。这是我的示例代码:
const {useState} = React;
const Example = ({title}) => {
const [count, setCount] = useState(0);
const [countInterval, setCountInterval] = useState(0)
let intervalID
const handleCount = () => {
setCount(count + 1)
console.log(count)
}
const progress = () => {
intervalID = setInterval(() => {
setCountInterval(countInterval => countInterval + 1)
console.log(countInterval)
if(countInterval > 100) { // this is never reached
setCountInterval(0)
clearInterval(intervalID)
}
},100)
}
const stopInterval = () => {
clearInterval(intervalID)
}
return (
<div>
<p>{title}</p>
<p>You clicked {count} times</p>
<p>setInterval count { countInterval } times</p>
<button onClick={handleCount}>
Click me
</button>
<button onClick={progress}>
Run interval
</button>
<button onClick={stopInterval}>
Stop interval
</button>
</div>
);
};
// Render it
ReactDOM.render(
<Example title="Example using simple hook:" />,
document.getElementById("app")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
如果我通过handleCount
设置状态,一切都会按预期发生,但是当我运行progress
函数时,在setInterval内部countInterval
值根本不会改变。无论如何,该州的countInterval
都发生了变化。
为了解决这个问题,我在函数中使用progress
变量,如下所示:
const progress = () => {
let internalValue = 0
intervalID = setInterval(() => {
setCountInterval(internalValue)
internalValue++
if(internalValue > 100) {
setCountInterval(0)
clearInterval(intervalID)
}
},100)
}
这很好用,但我仍然想知道是否有更好的方法以及我在第一种情况下做错了什么。
第二个问题是我无法清除progress
函数之外的间隔,我不确定我在这里做错了什么或我错过了什么?提前感谢您的任何帮助和建议。
您的问题是由重新渲染时丢失计时器引用以及setInterval
回调引用过时版本的setCountInterval
引起的。
为了使它完全正常工作,我建议添加一个状态变量来跟踪它是否已启动,并添加一个useEffect
来处理setInterval
的设置和清除。
const Example = ({ title }) => {
const [count, setCount] = useState(0);
const [countInterval, setCountInterval] = useState(0);
const [started, setStarted] = useState(false);
const handleCount = () => {
setCount(count + 1);
console.log(count);
};
useEffect(() => {
let intervalID;
if (started) {
intervalID = setInterval(() => {
setCountInterval(countInterval => countInterval + 1);
console.log(countInterval);
if (countInterval > 100) {
setCountInterval(0);
setStarted(false);
}
}, 100);
} else {
clearInterval(intervalID);
}
return () => clearInterval(intervalID);
}, [started, countInterval]);
return (
<div>
<p>{title}</p>
<p>You clicked {count} times</p>
<p>setInterval count {countInterval} times</p>
<button onClick={handleCount}>Click me</button>
<button onClick={() => setStarted(true)}>Run interval</button>
<button onClick={() => setStarted(false)}>Stop interval</button>
</div>
);
};
此处的工作沙盒:https://codesandbox.io/s/elegant-leaf-h03mi
countInterval
是一个局部变量,包含一个基元值。根据 JavaScript 的本质,setState
无法以任何方式改变该变量。相反,它会重新执行整个Example
函数,然后useState
将返回新的更新值。这就是在组件重新渲染之前无法访问更新状态的原因。
通过将条件移动到状态更新回调中,可以轻松解决您的问题:
setCountInterval(countInterval => countInterval > 100 ? 0 : countInterval + 1)
由于重新渲染,您不能使用局部变量,因此每次重新渲染时都会重新创建intervalId
变量(并且其值会丢失(。使用useRef
跨重新渲染使用值。
const interval = useRef(undefined);
const [count, setCount] = useState(0);
function stop() {
if(!interval.current) return;
clearInterval(interval.current);
interval.current = null;
}
function start() {
if(!interval.current) interval.current = setInterval(() => {
setCount(count => count > 100 ? 0 : count + 1);
});
}
useEffect(() => {
if(count >= 100) stop();
}, [count]);