问题描述:
有时候我们的 effect 可能会使用一些频繁变化的值。而我们可能会忽略依赖列表中的state,但这通常会引起 Bug,例如:
function MyCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state变量
}, 1000);
return () => clearInterval(id);
}, []); // Bug: `count` 没有被指定为依赖
return {count}
;
}
这里传入空的依赖数组 []
,意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会存在问题,在 setInterval
的回调中,count
的值不会发生变化。因为当 effect 执行时,会创建一个闭包,并将 count
的值被保存在该闭包当中,且初值为 0
。每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count
永远不会超过 1。
指定 [count]
作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval
在被清除前(类似于 setTimeout
)都会调用一次。但这并不是我们想要的。
解决方案:
要解决这个问题,我们可以使用setState的函数式更新形式。它允许我们指定 state 该如何改变而不用引用当前state:
function MyCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // ✅ effect 不使用组件作用域中的任何变量
return {count}
;
}
此时,setInterval
的回调依旧每秒调用一次,但每次 setCount
内部的回调取到的 count
是最新值(在回调中变量命名为 c
)。
在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),可尝试用useReducer Hook 把 state 更新逻辑移到 effect 之外。