经典案例
import { useEffect, useState } from "react";
const HookClosure = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []); // [] 表示组件 effect 只会执行一次
return <div>{count}</div>;
};
export default HookClosure;
思考一下,页面会如何展示!
页面从 0 变成 1 然后不会有任何变化了!
这个就是很典型的 Hooks
闭包陷阱问题!
闭包解析
在 HookClosure
组件中,useEffect
回调函数是一个内部函数(被传递给 useEffect
来执行特定的副作用逻辑)
它嵌套在 HookClosure
这个函数组件内部。在 useEffect
的回调函数中,访问了在组件作用域内定义的 count
变量(通过 setCount(count + 1)
这样的语句)。
闭包形成的关键 —— 对外部变量的持久引用
当
HookClosure
组件首次渲染时,useEffect
的回调函数会被创建并保存下来(因为传递给useEffect
的第二个参数[]
表示只在组件挂载时执行一次,所以这个回调函数后续会一直存在)。此时,它就形成了一个闭包,其中包含了count
变量 第一次保存的时候count
为 0。
三种解法
1. setData 回调函数的参数
import { useEffect, useState } from "react";
const HookClosure = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount((count) => count + 1);
}, 1000);
}, []); // [] 表示组件 effect 只会执行一次
return <div>{count}</div>;
};
export default HookClosure;
setCount
接收一个回调函数时,React 会将回调函数中的count
参数自动设置为当前状态的最新值
虽然回调函数本身处于定时器这个相对独立的环境中(定时器每次触发时都会执行这个回调函数),但 React 通过传递最新状态值给回调函数中的 count
参数,使得每次更新 count
状态时,都是基于上一次更新后的正确值来进行加 1
操作。
2. 更新依赖数组
当然也会有在外面使用的情况,不能每次都依靠setData 的回调参数拿最新的值啊!
比如这里,setDoubleCount 就用到了外面的 count,形成了闭包,但又不能把它挪到 setState 里去写:
import { useEffect, useState } from "react";
const HookClosure = () => {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);
function double(num: number) {
return num * 2;
}
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
setDoubleCount(double(count));
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // [] 表示组件 effect 只会执行一次
return (
<div>
<div>{count}</div>
<div>{doubleCount}</div>
</div>
);
};
export default HookClosure;
这个时候就需要把 count 当做依赖传入数组,当依赖变动的时候,会重新执行 effect。
import { useEffect, useState } from "react";
const HookClosure = () => {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);
function double(num: number) {
return num * 2;
}
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
setDoubleCount(double(count));
}, 1000);
return () => {
// 这里要记得清除当前 effect 的定时器 否则下一次执行 effect 也会创建定时器
clearInterval(timer);
};
}, [count]); // [] 表示组件 effect 只会执行一次
return (
<div>
<div>{count}</div>
<div>{doubleCount}</div>
</div>
);
};
export default HookClosure;
3. 使用 useRef
某些情况下可能希望定时器不要重置
比如下面的代码,可能页面进入的时候,定时器去按顺序去取渐变色!如果添加了 count 依赖,每次定时器重新执行,定时器内部的顺序
startIdx,endIdx
都会重置,背景色就不会按照顺序持续变化,而是一直展示第一种。【强行使用的 demo🤣】
import { useEffect, useState } from "react";
const colors = ['red', 'green', 'blue'];
const HookClosure = () => {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);
function double(num: number) {
return num * 2;
}
useEffect(() => {
let startIdx = 0
let endIdx = 0
const timer = setInterval(() => {
startIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
endIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
setCount(count + 1);
setDoubleCount(double(count));
const body = document.body;
body.style.background = `linear-gradient(to right, ${colors[startIdx]}, ${colors[endIdx]})`;
}, 1000);
return () => {
clearInterval(timer);
};
}, [count]); // [] 表示组件 effect 只会执行一次
return (
<div>
<div>{count}</div>
<div>{doubleCount}</div>
</div>
);
};
export default HookClosure;
如果把count 依赖去掉,背景色就正常了,但是 count 的逻辑又回到了之前的问题
import { useEffect, useState } from "react";
const colors = ['red', 'green', 'blue'];
const HookClosure = () => {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);
function double(num: number) {
return num * 2;
}
useEffect(() => {
let startIdx = 0
let endIdx = 0
const timer = setInterval(() => {
startIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
endIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
setCount(count + 1);
setDoubleCount(double(count));
const body = document.body;
body.style.background = `linear-gradient(to right, ${colors[startIdx]}, ${colors[endIdx]})`;
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // [] 表示组件 effect 只会执行一次
return (
<div>
<div>{count}</div>
<div>{doubleCount}</div>
</div>
);
};
export default HookClosure;
下面的代码可以完美解决!
通过 useRef 创建 ref 对象,保存执行的函数
每次执行setCount都要重新渲染 setIntervalCallBackRef 都会更新最新的 setIntervalCallBack
而 setIntervalCallBack 内部的 state 也都是最新的。
import { useEffect, useRef, useState } from "react";
const colors = ['red', 'green', 'blue'];
const HookClosure = () => {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(0);
function double(num: number) {
return num * 2;
}
const setIntervalCallBackRef = useRef(setIntervalCallBack)
function setIntervalCallBack() {
setCount(count + 1);
setDoubleCount(double(count));
}
setIntervalCallBackRef.current = setIntervalCallBack;
useEffect(() => {
let startIdx = 0
let endIdx = 0
const timer = setInterval(() => {
startIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
endIdx = startIdx + 1
const body = document.body;
body.style.background = `linear-gradient(to right, ${colors[startIdx]}, ${colors[endIdx]})`;
setIntervalCallBackRef.current()
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // [] 表示组件 effect 只会执行一次
return (
<div>
<div>{count}</div>
<div>{doubleCount}</div>
</div>
);
};
export default HookClosure;
这样,定时器执行的函数里就始终引用的是最新的 count。
useEffect 只跑一次,保证 setIntervel 不会重置,是每秒执行一次。
执行的函数是从 ref.current 取的,这个函数每次渲染都会更新,引用着最新的 count。
补充对比
Js 经典闭包案例
function Count() {
let counter = 0;
return () => {
counter += 1;
return counter;
};
}
let counter = Count()
console.log(counter())
console.log(counter())
console.log(counter())
console.log(counter())
为什么这个案例的 counter 能拿到最新的!
但是 React 的却不行
其实很简单,因为 Effect
只执行了一次!所以每次回调获取到的 count
都是 0 !
评论区