我刚刚开始学习 React,我正在观看一个涉及状态和钩子的教程。它只是每 1000 毫秒处理一次更新时间(或者我是这么认为的)。
import React from "react";
let count = 0;
function App() {
const now = new Date().toLocaleTimeString();
let [time, setTime] = React.useState(now);
function updateTime(){
const newTime = new Date().toLocaleTimeString();
setTime(newTime);
count++;
console.log(count);
console.log(new Date().getMilliseconds());
}
setInterval(updateTime, 1000);
return (
<div className="container">
<h1>{time}</h1>
<button onClick = {updateTime}>time</button>
</div>
);
}
export default App;
本教程的目的只是一个关于如何更新时间的简单示例,但我注意到它每 1000 毫秒更新多次(突发)。我怀疑每次对钩子的更改都会呈现新组件,但旧组件仍然在那里更新并生成更多组件,导致每 1000 毫秒的调用似乎呈指数级增长。
我很好奇这里发生了什么?假设有一个每 1000 毫秒更新一次的简单计数器,我该怎么做?
setTime(count)
显然行不通
问题:在您当前的实现中,每次组件渲染时都会调用
setInterval
(即,在设置时间状态后也将调用),并且将创建一个新的间隔 - 这产生了这种“指数增长”如您的控制台中所示。
useEffect
将是处理这种情况的最佳方法。看看我下面的例子。 useEffect
这里只会在初始组件渲染后(当组件安装时)运行。
React.useEffect(() => {
console.log(`initializing interval`);
const interval = setInterval(() => {
updateTime();
}, 1000);
return () => {
console.log(`clearing interval`);
clearInterval(interval);
};
}, []); // has no dependency - this will be called on-component-mount
如果您想运行效果并仅清理一次(在安装和 unmount),您可以传递一个空数组([])作为第二个参数。这 告诉 React 你的效果不依赖于 props 中的任何值 或状态,所以它永远不需要重新运行。
在您的场景中,这是“空数组作为第二个参数”的完美用法,因为您只需要在安装组件时设置间隔并在卸载时清除间隔。看看
useEffect
返回的函数。这是我们的清理函数,它将在组件卸载时运行。这将“清理”,或者在这种情况下,当组件不再使用时清除间隔。
我编写了一个小应用程序,演示了我在这个答案中涵盖的所有内容:https://codesandbox.io/s/so-react-useeffect-component-clean-up-rgxm0?file=/src/App .js
我合并了一个小型路由功能,以便可以观察到组件的“卸载”。
我的旧答案(不推荐):
每次组件重新渲染时都会创建一个新的间隔,这就是您为时间设置新状态时发生的情况。我要做的是在设置新的间隔之前清除之前的间隔(
clearInterval
)
try {
clearInterval(window.interval)
} catch (e) {
console.log(`interval not initialized yet`);
}
window.interval = setInterval(updateTime, 1000);
https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval
Macro Amorim 回答说,
useEffect
是做到这一点的最佳方法。代码在这里:
useEffect(() => {
const interval = setInterval(() => {
const newTime = new Date().toLocaleTimeString();
setTime(newTime);
}, 1000)
return () => {
clearInterval(interval);
}
}, [time])
你也可以试试这个 -
import React, { useEffect, useState } from 'react';
export default function App() {
const [timer, setTimer] = useState(0);
const [toggle, setToggle] = useState(false);
useEffect(() => {
let counter;
if (toggle) {
counter = setInterval(() => setTimer(timer => timer + 1), 1000);
}
return () => {
clearInterval(counter);
};
}, [toggle]);
const handleStart = () => {
setToggle(true);
};
const handleStop = () => {
setToggle(false);
};
const handleReset = () => {
setTimer(0);
setToggle(false);
};
return (
<div>
<h1>Hello StackBlitz!</h1>
<p>Current timer - {timer}</p>
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handleReset}>Reset</button>
<button onClick={handleStop}>Stop</button>
</div>
);
}
在这种情况下,你可以使用 React 中的 useEffect 钩子,在函数内部返回时,你可以使用clearInterval 函数,我建议你查一下,useEffect 非常适合你想要做的事情。
使用 Hooks 的简单定时器
import React, { useEffect, useState } from "react";
const TimeHeader = () => {
const [timer, setTimer] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => {
setTimer(new Date());
}, 1000);
}, []);
return (
<div className="electonTimer">
<div className="electionDate">
<h1>{timer.toLocaleTimeString()}</h1>
</div>
</div>
);
};
以上答案均不能满足我的要求。如果我想从父组件传递计时器怎么办?如果倒计时器值是一个状态怎么办?接受的答案在渲染后立即开始间隔,这在许多情况下可能会很糟糕!该倒计时组件的用户必须卸载倒计时组件才能运行清理代码,这意味着如果我们从未卸载该组件(通过条件渲染或其他方式),我们可能永远不会最终调用清理代码,这是一个很大的缺陷。
我对这些问题的解决办法如下:
import React, { useEffect, useRef } from "react";
export type Props = {
seconds: number;
decreaseCountDown: () => void;
};
export const MIN_TIMER_VALUE = -1;
const CountDownTimer = ({ seconds, decreaseCountDown }: Props) => {
const intervalId = useRef<NodeJS.Timer | undefined>(undefined);
useEffect(() => {
createIntervalIfRequired();
return () => clearIntervalIfRequired();
}, [seconds]);
const createIntervalIfRequired = () => {
if (intervalId.current !== undefined) {
return;
}
if (seconds <= MIN_TIMER_VALUE) {
return;
}
const createdIntervalId = setInterval(() => {
decreaseCountDown();
}, 1000);
intervalId.current = createdIntervalId;
};
const clearIntervalIfRequired = () => {
if (seconds > MIN_TIMER_VALUE || intervalId.current == undefined) {
return;
}
clearInterval(intervalId.current);
intervalId.current = undefined;
};
return (
<span className={`px-2 ${seconds <= MIN_TIMER_VALUE ? "hidden" : ""}`}>
{beautifyTime(seconds)}
</span>
);
};
const beautifyTime = (time: number): string => {
let minutes = parseInt((time / 60).toString()).toString();
let seconds = parseInt((time % 60).toString()).toString();
if (seconds.length == 1) {
seconds = "0" + seconds;
}
if (minutes.length == 1) {
minutes = "0" + minutes;
}
return `${minutes}:${seconds}`;
};
export default CountDownTimer;
这样,只有当 props
seconds
大于 0 时,我们才会创建间隔,并且即使我们的组件没有卸载,我们也会清理间隔! 如果我们现在没有任何间隔旋转,我们只会开始一个新的间隔。
该组件的使用者将执行以下操作:
<CountDownTimer
seconds={countDownTimer}
decreaseCountDown={() => {
setCountDownTimer((currentCounter) => currentCounter - 1);
}}
/>
这样消费者不关心拆卸组件或创建多少个间隔。它只知道当前的 seconds
值是多少。**** 即使在react.dev 文档上,他们也使用
useRef
钩子来表示intervalId。**