react hooks 中的“闭包陷阱”
问题描述
开发当中遇到了一个问题,当在 useEffect 中使用 setInterval 定时器时,定时器的函数参数中使用到的 state 无法获取到最新的状态。看代码:
function ADD(){
const [count, setCount] = useState(1);
const fn = () => {
setInterval(() => {
console.log(count);
}, 1000);
};
useEffect(fn, []);
const addCount = () => {
setCount(pre => ++pre);
};
return (
<Button onClick={addCount}> count++ </Button>
);
}
预期的效果
在点击按钮修改 count 的值之后,希望定时器对于 count 的打印也能更新为最新的值。
实际效果
不管你点击多少次按钮,定时器都会亘古不变地打印 count 的初始值 1。
原因分析
由于 useEffect 的依赖项数组是空数组,所以 useEffect 只会在第一次页面渲染的时候执行一次回调函数 fn,即定时器就在这个时候执行了一次,而此时定时器拿到的 count 值就是 1,并且定时器的回调函数 () => {console.log(count)}
拿到的 count 值是 fn 形成的闭包传递进去的 count 值。因为 fn 只会在第一次页面渲染的时候执行,以后 count 改变也不会触发 fn 的再次执行(因为依赖项的数组为空),所以定时器的回调函数 () => {console.log(count)}
拿到的 count 值也是不会更新的。
解决办法
找到了问题发生的原因,也就是 定时器的回调函数 () => {console.log(count)}
拿到的 count 值总是 fn 形成的闭包传递进来的 count 值 ,那么着手点就是要 更新 fn 形成的闭包传递进来的 count 值 也就是 更新 fn 。想要更新 fn 很简单,只需要给 useEffect 添加 count 作为依赖,那么每次 count 改变之后就会重新执行 fn 拿到新的 count 值。(涉及多次设置定时器,不要忘了每次更新定时器之前销毁之前的定时器)
function ADD(){
const [count, setCount] = useState(1);
const fn = () => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => {
clearInterval(timer);
};
};
useEffect(fn, [count]);
const addCount = () => {
setCount(pre => ++pre);
};
return (
<Button onClick={addCount}> count++ </Button>
);
}
以上,就达到了最开始想要实现的效果了。
新的问题
由于我使用的是一个自定义 hook ,基于 useEffect 和 useInterval 封装的,无法手动添加依赖 ,既然无法添加依赖项,就不能使用上述方案解决问题了。
也就是,定时器的回调函数只能拿到第一次的渲染时拿到的数据。
思路: 如果定时器需要使用的这个数据是一个引用类型的值,那么只要我在修改这个引用类型的值的时候只修改其中的属性值,不修改引用,那么就能够保证定时器拿到的数据地址没有变,但是其中的属性的值可以改变。那么定时器使用的虽然是第一次拿到的引用类型数据,但是在访问其中的属性时却可以拿到最新的属性数据。
解决办法
直接上代码:
function ADD(){
const [date, setDate] = useState({count: 1}); // 这里使用数组也可以哦,只要是引用类型的值就行
const fn = () => {
setInterval(() => {
console.log(date.count);
}, 1000);
};
useEffect(fn, []);
const addCount = () => {
// 一定不能修改对象的引用
setDate(pre => Object.assign(pre, {count: ++pre}));
};
return (
<Button onClick={addCount}> count++ </Button>
);
}
以上算是解决了我遇到的问题。
另一个新的问题
通过不修改对象引用的方式 setDate(pre => Object.assign(pre, {count: ++pre}));
修改引用类型的 state 值之后,在 useEffect 的依赖项数组中即使添加了该 state 也无法监听其变化,因为该 state 具有引用相等性。当既想要接触引用类型状态的闭包陷阱,又想要监听该引用类型状态的变化时,使用 Object.assign 就不能满足要求了。 这时可以使用 useRef 对该引用类型(或非引用类型)的值进行一个包装,因为每个使用 useRef 创建的对象的引用都是不可变的,此时在修改状态时保持每次都会修改该状态的引用,在 useEffect 中的定时器中使用 useRef 返回的对象依然可以读取到最新更新的状态的值。
方式如下:
const countRef = useRef<number>();
rangeRef.current = count;
为什么许多解释 react hook 闭包陷阱的文章都提及了 useRef
许多文章在解释 react hook 闭包陷阱的文章中都提及了 useRef 能够解决这个问题,那么这是为什么呢?其实 useRef 解决闭包陷阱的原理就是上述的引用的不变性。
组件每一次渲染时 useRef 返回的都是同一个对象(即引用不变),例如每次执行 ref = useRef() 所返回的都是同一个对象引用,那么每次通过这个引用去取其中的属性中的值的时候,自然就能够拿到最新的数据了。
todo
复杂状态的管理使用 useReducer ->
react hook 的执行原理及存储结构 ->