产生场景
- 事件绑定。比如示例代码中,在页面最初渲染完成后只绑定一次事件的情况,比如使用
echarts,在useEffect中获取echarts的实例并绑定事件 - 定时器。页面加载后注册一个定时器,定时器内的函数也会产生如此的闭包问题。
问题代码(一)
看一段因为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';
console.log(size)
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
makeSizer(size) 返回的函数内部会捕获 size 参数。当调用 size12() || size14() || size16() 时,返回的闭包始终记住 size 是 12/14/16,哪怕外部再有 size 变量变化,这个闭包里的 size 永远是最初传入的值。
解决方法
要让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);
};
}, []);
问题代码(二)
useEffect(() => {
if (aiLoading) {
fetchAiResultMutation.mutate(
{ ... },
{
onSuccess: res => {
console.log('AI动画', aiLoading);
handleTypedText(res?.data?.output);
},
}
);
}
}, [aiLoading]);
遇到的问题:
- 当点击触发AI生成,把aiLoading设为true,接口开始请求。
- 接口请求时间比较长,比如3s,此时如果切换用户,aiLoading设为false,但请求还在进行。
- 等fetchAiResultMutation接口返回,onSuccess回调被执行,打印的aiLoading依然是true
原因分析
- 在
useEffect中,fetchAiResultMutation.mutate被调用时,传入的onSuccess回调会捕获当时的aiLoading状态。即使接口在调用期间不断地切换用户,把aiLoading设为false,但onSuccess回调是在mutate调用那一刻“快照”捕获的上下文。 - 这里的
onSuccess函数在mutate触发的一刻就已经绑定了当时的aiLoading值(true),即使后续setAiLoading(false),回调里的aiLoading依然是true。
解决方法
用 useRef 保存最新值