React组件渲染后旧useEffect仍持续运行的原因探究
我用React实现秒表功能,当前代码如下:
const App: React.FC = () => { const [seconds, setSeconds] = useState(0); const [isPaused, setIsPaused] = useState(false); const secondsToTimerFormat = (seconds: number): string => { console.log(seconds) return (seconds-seconds%60)/60+":"+seconds%60 } const manipulateTimer = (toPauseTimer: boolean) => { setIsPaused(toPauseTimer); } useEffect(() => { if(!isPaused){ setTimeout(() => { setSeconds(seconds + 1) }, 1000) } }, [seconds, isPaused]) return ( <div className="App"> {secondsToTimerFormat(seconds)} <div> <button onClick={() => {manipulateTimer(true)}}>Pause</button> <button onClick={() => {manipulateTimer(false)}}>Resume</button> <button onClick={() => { setSeconds(0); }}>Reset</button> </div> </div> ); }
预期功能正常,但Reset按钮表现不符合预期:当运行13秒后点击Reset,console.log输出序列为:9, 10, 11, 12, 13, 0, 14, 1, 15, 2, 16, 3, 17, 4, 18, 5...。我已知通过在useEffect内添加清理函数可以修复,但想了解当前行为的原因:点击Reset时,存在一个捕获了seconds=13的useEffect对应的定时器在运行,1秒后结束,同时我执行setSeconds(0),为何会出现旧的计时流和新的计时流并行的情况?
原因解析
useEffect的闭包特性与独立渲染周期
每个渲染周期的useEffect都是独立的,它会捕获当前渲染时的seconds和isPaused值。当秒表运行到13秒时,此时的useEffect捕获了seconds=13、isPaused=false,并注册了一个1秒后执行的定时器,任务是调用setSeconds(13+1)。未清理的定时器堆积
点击Reset触发setSeconds(0),此时会触发新的渲染,seconds变为0,对应的新useEffect会注册另一个定时器,任务是1秒后调用setSeconds(0+1)。但之前那个捕获seconds=13的定时器并没有被清理,它会在1秒后正常执行,触发setSeconds(14),这又会触发新的渲染,生成新的useEffect和定时器,继续执行setSeconds(14+1)。并行计时流的形成
此时页面上存在两个独立的定时器流:
- 旧的流:从13延续,依次触发
14→15→16...的state更新,每个更新都会生成新的定时器 - 新的流:从Reset后的0开始,依次触发
1→2→3...的state更新,每个更新也会生成新的定时器
两个流的定时器交替触发state更新,就导致了console.log中出现交替的数值序列,看起来像是旧的useEffect在多次运行,本质是未清理的定时器引发的多任务堆积。
内容的提问来源于stack exchange,提问作者Light Yagami

