难道是React的问题?
react
中经常会出现闭包问题
,最经典的一个案例
就是延迟弹出弹窗
:
在这个例子中,我们会发现,当我们点击按钮延迟三秒调用alert
期间,我们疯狂点击让count+1
,但是最后还是显示了0
。为了搞清为什么会出现这种情况,首先我们要明白两个东西
:
- 函数是个
特殊的对象
- 函数的
作用域
是静态作用域
为什么函数是个特殊的对象
跟这个有关,第一点就是函数上有个内部的[[Scopes]]属性
,这个属性,决定了当时函数所在的作用域
,我们改造刚才那个弹出的click
,把它改成这样:
const handleClick = () => {
const fn = () => {
alert(count)
};
console.dir(fn)
setTimeout(fn, 3000)
};
查看输出中的[[Scopes]]
,可以看到函数闭包中
引用了count
:
此时count
为0
。也就是说,作用域
没有发生变化。
之后我们就要明白第二个点函数的作用域是静态作用域
。就是说,函数的作用域
是创建的时候就决定了,而我们点击setCount
的时候,实际上Test
函数被重新刷新了,而此时fn
还是第一次的函数对象
,为了更直观的查看这个,我们再次改造弹出的click
:
const handleClick = () => {
const fn = () => {
alert(count)
};
setTimeout(() => {
console.dir(fn);
fn();
}, 3000)
};
可以看到,setTimeout
的fn
在回调触发时,环境还是第一次的环境
。
那既然这样,怎么解决这个问题呢,其实也很简单,把count
变成reference值
就行了,也就是使用useRef
const Test = function () {
// const [count, setCount] = useState(0);
const [update, setUpdate] = useState(0); // 提交状态刷新Test
const count = useRef(0); // 使用ref
const handleClick = () => {
const fn = () => {
alert(count.current)
};
setTimeout(() => {
console.dir(fn);
fn();
}, 3000)
};
const handleAddClick = () => {
count.current += 1;
setUpdate(!update)
}
return (<>
<button onClick={handleAddClick}>{count.current}</button>
<button onClick={handleClick}>弹出</button>
</>
);
};
可以看到解决了这个问题,当然我们可以查看此时的fn
:
可以看到因为使用useRef
,所以此时是实时
的。也就是说,react
的闭包问题
根本原因是函数对象的作用域的问题
谨慎使用useCallback
因为闭包问题
的存在,useCallback
就显得需要谨慎使用
了,因为一旦刷新不及时,那返回的函数是不及时的。再看一个例子,点击count+1
,但是useCallback
包装的函数使用[]
依赖:
const Test = function () {
const [count, setCount] = useState<number>(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, []); // 空依赖
return <div onClick={handleClick}>click me {count}</div>;
};
可以看到,此时我无论怎么点击,count
最多就+1
,主要原因就是此时useCallback
没有返回最新的函数
对象,我们改造一下,显示的更直观点:
const Test = function () {
const [count, setCount] = useState<number>(0);
const handleClick = useCallback(function a() {
console.dir(a); // 输出a函数
setCount(count + 1);
}, []); // 空依赖
return <div onClick={handleClick}>click me {count}</div>;
};
可以看到,无论怎么点击,a
函数作用域
中的count
都是0
,所以导致每次都是0+1
。当然平时可以使用ahooks
的useMemoizedFn
,附上源码。