问题代码
看一段因为useEffect
导致的闭包问题代码
const btn = useRef();
const [v, setV] = useState('');
useEffect(() => {
let clickHandle = () => {
console.log('v:', v);
}
btn.current.addEventListener('click', clickHandle)
return () => {
btn.current.removeEventListener('click', clickHandle)
}
}, []);
const inputHandle = e => {
setV(e.target.value)
}
return (
<>
<input value={v} onChange={inputHandle} />
<button ref={btn} >测试</button>
</>
)
useEffect
的依赖项数组为空,所以在页面渲染完成之后,内部代码只会执行一次,页面销毁再执行一次。此时在输入框中输入任意字符,再点击测试
按钮,得到的输出为空,之后无论如何输入任何字符,再点击测试
按钮时,输出的结果仍为空。
为什么会这样呢?其实就是闭包所造成的。
产生原因
-
闭包捕获了旧的
v
值- 在
useEffect
内部定义的clickHandle
函数是一个闭包,它捕获了组件作用域中的v
变量。 - 由于
useEffect
的依赖数组为空,它只在组件加载时执行一次。此时,v
的初始值是空字符串''
。 - 因此,
clickHandle
函数捕获的v
始终是初始的空值,而不是后续更新的值。
- 在
-
v
的更新不触发useEffect
重新执行- 当你在输入框中输入字符时,
inputHandle
函数调用setV
更新v
的值,这会触发组件重新渲染。 - 但在重新渲染时,
useEffect
不会重新执行,因为依赖数组为空。因此,clickHandle
函数仍然引用的是初始的空v
。
- 当你在输入框中输入字符时,
-
闭包中的
v
不是最新的- 闭包捕获的是变量的引用,而不是变量的值。因此,即使
v
的值在后续被更新,闭包中的v
仍然指向v
的初始引用,而这个引用的值已经被更新。 - 但在这种情况下,
v
的值确实发生了变化,但闭包中的v
仍然指向旧的引用,导致输出为空。
- 闭包捕获的是变量的引用,而不是变量的值。因此,即使
用更通俗易懂的闭包示例来理解
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
// 在 React 组件中,如果`useEffect`的依赖数组为空(`[]`),它的行为类似于`makeSizer`只调用一次
-
闭包捕获的是“变量的引用”,而不是“变量的值” :
- 闭包记住的是变量的地址,而不是变量的值。因此,如果变量的值发生了变化,闭包仍然可以通过地址访问到最新的值。
- 但在useEffect的例子中,闭包捕获的
v
是组件作用域中的一个变量。由于useEffect
只执行一次,闭包中的v
始终引用的是组件第一次渲染时的v
,而不是后续更新的v
。
-
useEffect
的依赖数组为空:- 由于依赖数组为空,
useEffect
只在组件加载时执行一次。因此,clickHandle
函数只捕获了v
的初始值(空字符串),而不会随着v
的更新而更新。
- 由于依赖数组为空,
-
v
的更新触发重新渲染:- 当
v
被更新时,组件重新渲染,v
的值发生了变化。但clickHandle
函数仍然引用的是组件第一次渲染时的v
,而不是最新的v
。
- 当
产生场景
- 事件绑定。比如示例代码中,在页面最初渲染完成后只绑定一次事件的情况,比如使用
echarts
,在useEffect
中获取echarts
的实例并绑定事件 - 定时器。页面加载后注册一个定时器,定时器内的函数也会产生如此的闭包问题。
解决方法
要让clickHandle
能够访问最新的v
值,可以采取以下两种方法:
方法一:将v
添加到useEffect
的依赖数组中
useEffect(() => {
let clickHandle = () => {
console.log('v:', v);
};
btn.current.addEventListener('click', clickHandle);
return () => {
btn.current.removeEventListener('click', clickHandle);
};
}, [v]); // 将v添加到依赖数组中
- 这样,每当
v
的值发生变化时,useEffect
会重新执行,重新定义clickHandle
函数,使其引用最新的v
值。
方法二:使用useRef
保存v
的最新值
const btn = useRef();
const [v, setV] = useState('');
const vRef = useRef(v);
useEffect(() => {
let clickHandle = () => {
console.log('v:', vRef.current);
};
btn.current.addEventListener('click', clickHandle)
return () => {
btn.current.removeEventListener('click', clickHandle);
};
}, []);