如题,最近在工作中碰到了使用一个问题,这边简化一下相关代码,
import { useEffect, useRef, useState } from "react";
export default function Home(){
const [count,setCount] = useState(0);
const btn = useRef(null);
const increment = () => {
setCount(count+1);
};
useEffect(() => {
btn.current.addEventListener('click',increment);
return () => {
btn.current.removeEventListener('click',increment);
}
}, [])
useEffect(() => {
console.log('count now', count);
},[count]);
return (
<div>
<h1>{count}</h1>
<button ref={btn} >加一</button>
</div>
)
}
发现,即使反复点击加一,页面显示的代码块也会停留在1
原因
这是由于在注册事件时函数的作用域就已经被决定了,通过查验作用域能证明这一点
那么由此可见,监听函数中用到的数据全被闭包保存,所以内部的count值实际上已然被确定了,而这边的setCount更新函数实际上与外界是同一个,这点可以通过查验react源码确定,这样导致的结果便是在触发监听事件时,虽然能通过setCount正确给外界的count赋值,但是由于setCount(count+1)中的count是内部保存的count,导致每次实际执行的是setCount(0+1),于是造成了错误;
解决方法
1.可以通过在依赖数组中添加state值来解决,由于根本原因是count的旧值,所以只需要在每次count更新时更新监听事件就行了
useEffect(() => {
btn.current.addEventListener('click',increment);
return () => {
btn.current.removeEventListener('click',increment);
}
}, [count])
2.由于setCount能够正常使用,而其中其实通过闭包保存了真实count的值,在调用函数时其可以传入一个函数,函数的第一个入参就是真实的count值,所以也可以通过这样更新
const increment = () => {
setCount((c) => c + 1);
};
3.可以使用useRef替代,由于前方问题得原因是创建函数时就确定了作用域,那么只要传入的不是具体数值,而是对象的引用地址,那么在使用的时候实际还是对同一个对象做出的操作,也就能保证数据的一致,useR俄方、就能完成这样的要求
const count = useRef(0);
const increment = () => {
count.current += 1;
};
总结
在useEffect依赖数组为空的情况下,添加监听器或类似setInterval的事件时,要注意内部有无用到外界state的值,由于useEffect特性,依赖数组为空的时候只会在初次渲染后执行,导致外界state更新时其添加的监听器不能得到新值,由此可能导致一系列问题