setInterval + React 钩子导致组件内多次更新
setInterval + React hooks causing multiple updates within component
我正在构建一个以秒为单位显示时间的秒表 UI。单击一个按钮,计时器将开始向上计数,再次单击时停止。用户应该能够再次启动它。
我遇到的问题是我可以让 setInterval
正常工作,但是一旦我包含 setTime
挂钩,组件就会更新以在 UI 中呈现时间,但是setInterval
实例被多次调用。这会导致奇怪的渲染行为。
const Timer = () => {
const [time, setTime] = useState(0)
let timer
const startStopTimer = () => {
if (!timer) timer = setInterval(() => setTime(time++), 1000)
else {
clearInterval(timer)
timer = null
}
}
return (
<div>
<p>Time: {time} seconds</p>
<Button
onClick={() => {
startStopTimer()
}
> Start/Stop </Button>
</div>
)
}
示例行为是:
- 用户点击Start/Stop
- 定时器从0开始向上计数
- 用户点击Start/Stop
- 计时器立即停止
- 用户点击Start/Stop
- 计时器从停止处继续
这是 React 钩子中陈旧闭包的经典示例,在调用 setTime
后,您的 setInterval 值 time
没有改变。更改您的代码:
setInterval(() => setTime(currentTime => currentTime + 1), 1000)
.
setTime
就像有类组件的 setState
一样,也接受一个回调函数,该函数将当前值作为第一个参数
此外,timer
变量在您的代码中是无用的,因为在每个 re-render 上它将是未定义的,您将无法访问 return 的 [=18] 值=],因此它将重新初始化 setInterval
。要处理 useRef
的使用,您可以将 setInterval
的 return 存储在 .current
中,这将在后续重新渲染后可供您使用,因此不再需要 re-init setInterval 你也可以使用 clearInterval
解决方案:
const {useState, useRef} = React;
const {render} = ReactDOM;
const Timer = () => {
const [time, setTime] = useState(0);
const timer = useRef(null);
const startStopTimer = () => {
if (!timer.current) {
timer.current = setInterval(() => setTime(currentTime => currentTime + 1), 1000);
} else {
clearInterval(timer.current);
timer.current = null;
}
};
return (
<div>
<p>Time: {time} seconds</p>
<button
onClick={startStopTimer}
>
Start/Stop
</button>
</div>
);
};
render(<Timer />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
这是一个使用反应 class 组件的示例。此示例跟踪开始时间,而不是在特定时间间隔内添加某个值。然后,当您停止计时器时,它会累积经过的时间。
传递给 setInterval
的回调可能并不总是每 n
毫秒准确调用一次。如果 JavaScript 引擎正忙,则可能需要多花几毫秒。保持计数器运行的时间越长,就会慢慢抵消实际经过的时间。
const {Component} = React;
const {render} = ReactDOM;
class StopWatch extends Component {
state = {startTime: null, accTime: 0, intervalId: null};
componentWillUnmount() {
clearInterval(this.state.intervalId);
}
ms() {
const {startTime, accTime} = this.state;
if (!startTime) return accTime;
return Date.now() - startTime + accTime;
}
start = () => {
this.setState({
startTime: Date.now(),
intervalId: setInterval(() => this.forceUpdate(), 10)
});
}
stop = () => {
clearInterval(this.state.intervalId);
this.setState({
startTime: null,
accTime: this.ms(),
intervalId: null
});
}
reset = () => {
this.setState({
accTime: 0,
startTime: this.state.startTime && Date.now()
});
}
render() {
return (
<div>
<h1>{this.ms() / 1000}</h1>
{this.state.startTime
? <button onClick={this.stop}>stop</button>
: <button onClick={this.start}>start</button>}
<button onClick={this.reset}>reset</button>
</div>
);
}
}
render(<StopWatch />, document.getElementById("stop-watch"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="stop-watch"></div>
我正在构建一个以秒为单位显示时间的秒表 UI。单击一个按钮,计时器将开始向上计数,再次单击时停止。用户应该能够再次启动它。
我遇到的问题是我可以让 setInterval
正常工作,但是一旦我包含 setTime
挂钩,组件就会更新以在 UI 中呈现时间,但是setInterval
实例被多次调用。这会导致奇怪的渲染行为。
const Timer = () => {
const [time, setTime] = useState(0)
let timer
const startStopTimer = () => {
if (!timer) timer = setInterval(() => setTime(time++), 1000)
else {
clearInterval(timer)
timer = null
}
}
return (
<div>
<p>Time: {time} seconds</p>
<Button
onClick={() => {
startStopTimer()
}
> Start/Stop </Button>
</div>
)
}
示例行为是:
- 用户点击Start/Stop
- 定时器从0开始向上计数
- 用户点击Start/Stop
- 计时器立即停止
- 用户点击Start/Stop
- 计时器从停止处继续
这是 React 钩子中陈旧闭包的经典示例,在调用 setTime
后,您的 setInterval 值 time
没有改变。更改您的代码:
setInterval(() => setTime(currentTime => currentTime + 1), 1000)
.
setTime
就像有类组件的 setState
一样,也接受一个回调函数,该函数将当前值作为第一个参数
此外,timer
变量在您的代码中是无用的,因为在每个 re-render 上它将是未定义的,您将无法访问 return 的 [=18] 值=],因此它将重新初始化 setInterval
。要处理 useRef
的使用,您可以将 setInterval
的 return 存储在 .current
中,这将在后续重新渲染后可供您使用,因此不再需要 re-init setInterval 你也可以使用 clearInterval
解决方案:
const {useState, useRef} = React;
const {render} = ReactDOM;
const Timer = () => {
const [time, setTime] = useState(0);
const timer = useRef(null);
const startStopTimer = () => {
if (!timer.current) {
timer.current = setInterval(() => setTime(currentTime => currentTime + 1), 1000);
} else {
clearInterval(timer.current);
timer.current = null;
}
};
return (
<div>
<p>Time: {time} seconds</p>
<button
onClick={startStopTimer}
>
Start/Stop
</button>
</div>
);
};
render(<Timer />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
这是一个使用反应 class 组件的示例。此示例跟踪开始时间,而不是在特定时间间隔内添加某个值。然后,当您停止计时器时,它会累积经过的时间。
传递给 setInterval
的回调可能并不总是每 n
毫秒准确调用一次。如果 JavaScript 引擎正忙,则可能需要多花几毫秒。保持计数器运行的时间越长,就会慢慢抵消实际经过的时间。
const {Component} = React;
const {render} = ReactDOM;
class StopWatch extends Component {
state = {startTime: null, accTime: 0, intervalId: null};
componentWillUnmount() {
clearInterval(this.state.intervalId);
}
ms() {
const {startTime, accTime} = this.state;
if (!startTime) return accTime;
return Date.now() - startTime + accTime;
}
start = () => {
this.setState({
startTime: Date.now(),
intervalId: setInterval(() => this.forceUpdate(), 10)
});
}
stop = () => {
clearInterval(this.state.intervalId);
this.setState({
startTime: null,
accTime: this.ms(),
intervalId: null
});
}
reset = () => {
this.setState({
accTime: 0,
startTime: this.state.startTime && Date.now()
});
}
render() {
return (
<div>
<h1>{this.ms() / 1000}</h1>
{this.state.startTime
? <button onClick={this.stop}>stop</button>
: <button onClick={this.start}>start</button>}
<button onClick={this.reset}>reset</button>
</div>
);
}
}
render(<StopWatch />, document.getElementById("stop-watch"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="stop-watch"></div>