hooks为什么会出现闭包
在使用React Hooks时可能会遇到一个比较常见的问题,就是React Hooks的闭包陷阱,这与React的更新机制有关,为了搞懂React Hooks出现的闭包陷阱,先了解下什么闭包。
闭包
闭包(closure)简单的来说就是定义在一个函数内部的函数,闭包可以让函数内部的变量保存在内存中,从而延长一个变量的生命周期。因为js特殊的变量作用域,一些高级的特性中都要靠闭包来实现。
代码举例
ts
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
useEffect(() => {
const time = setInterval(() => {
console.log(count);
setCount(count + 1);
}, 1000);
return () => clearInterval(time);
}, []);
return (
<>
<h2>{count}</h2>
</>
);
};
export default App;
App组件初次渲染执行useEffect中的定时器,每秒执行一次,将count值加1。运行代码后发现,count的值只变化了一次,而且打印的count值都是0。
这就是React闭包陷阱的一个典型场景。
首先,在首次执行定时器时,count值是0,setCount(count+1)
页面重新渲染,界面显示1。
然后在函数组件重新(每次)渲染时,都会重新执行函数体中的代码。但是在重新执行的过程中,count变量在闭包函数中,在首次渲染时捕获到函数内部的count值。而且在后续的渲染中即使count的值发生了变化,但之前捕获的变量值仍然保持着原来的值,就形成了闭包陷阱。
原因
根据上面的例子,我们可以给useEffect加上count依赖,这样界面就是我们期望的渲染逻辑,但是加上count依赖后,每次都会重新销毁和创建定时器,这就会导致性能损耗以及定时器中的逻辑并不是1s执行一次。useState暴露出来的setState函数还可以传递一个函数,函数的参数就是最新的state值。返回数据会作为setState的参数。
界面渲染正常,但是在定时器的代码中获取到的count值不对,还都是0。这是因为打印的count值还是从闭包中拿的,但是渲染的count值通过回调函数拿的是最新的。
而且上面说的解法依赖于useState提供的回调函数,但是hooks闭包陷阱不仅仅只在使用useState时会出现。闭包陷阱的出现是由于函数组件的更新机制产生的。
js
import React, { useEffect } from "react";
const Child = ({ onClick }: {onClick: () => void}) => {
console.log("render child......");
return (
<button onClick={onClick} >
Button
</button>
);
};
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
useEffect(() => {
const time = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(time);
};
}, []);
const handleClick = () => {
console.log("count:", count);
};
return (
<>
<h2>{count}</h2>
<Child onClick={handleClick} />
</>
);
};
export default App;
在上面这个例子中没有闭包陷阱,逻辑也都正确,但是有一个优化问题,每次重新渲染组件的时候handleClick都会被重新定义,从而导致Child组件重新渲染。我们使用useCallback包裹,然后用memo包裹Child组件优化一下。
每次count变化时,重新渲染App组件,Child组件不会重新渲染了,但是点击button,打印出来的count值永远都是0,这里就出现了React Hooks的闭包陷阱。
如何解决闭包陷阱
使用useRef。
使用useRef保存数据,然后每次需要最新的数据时在useRef的current中拿。
tsx
import React, { memo, useCallback, useEffect, useLayoutEffect, useRef } from "react";
const Child = memo(({ onClick }: {onClick: () => void}) => {
console.log("render child");
return (
<button onClick={onClick} >
Button
</button>
);
});
const App: React.FC = () => {
const [count, setCount] = React.useState(0);
const getCountInfo = useCallback(() => {
console.log("count:", count);
},[count])
const ref = useRef(getCountInfo);
useEffect(() => {
const time = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(time);
};
}, []);
useLayoutEffect(() => {
ref.current = getCountInfo
},[getCountInfo])
const handleClick = useCallback(() => {
ref.current()
}, [])
return (
<>
<h2>{count}</h2>
<Child onClick={handleClick} />
</>
);
};
export default App;
useEffect
没有依赖只执行一次。- 定时器每次的更新的count都是最新的。
useCallback
和memo做了优化,避免了不必要的渲染。- useRef保存了最新的getCountInfo函数,每次调用函数获取的都是最新的count数据。
总结
- 闭包陷阱的产生并不是hooks本身的问题,是因为在React的更新机制的基础上,使用hooks时,我们对变量的引用和更新处理不当时,就会容易出现闭包陷阱导致出现的数据不正确的问题。
- 一般的原因形成就是hooks函数中使用了state,但是因为各种原因没有在依赖数组中加上state依赖,导致hooks函数的回调中使用的state还是之前的。
- 使用useRef来解决闭包陷阱,因为useRef可以在组件中持久性保存数据。