react hooks 中的“闭包陷阱”

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 的执行原理及存储结构 ->

深度好文

react hook 闭包陷阱的几种解决方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值