# React Hooks 中的闭包陷阱
Table of Contents
React Hooks 中的闭包,指函数式组件每次 render 都会产生一个新的函数,这个函数会产生一个在当前阶段的闭包。
最近在工作中刚好遇到这个问题,经过一些资料的查阅,总结出一部分总结。
问题
以下是一个简化版本的代码示例:
const App = (): React.ReactElement | null => { const [count, setCount] = useState(0) const [handleCount, setHanleCount] = useState({ action: () => {}, })
useEffect(() => { setHanleCount({ action: () => setCount(count + 1), }) }, [])
return ( <div> <button onClick={handleCount.action}>Click</button> <div>{count}</div> </div> )}https://codesandbox.io/s/capture-2l2vq5
上面例子中,Button 组件的 action 需要依赖父组件传递。当我们点击了一次按钮之后,count 变成 1,之后再点击不会增加。
解决方案
如果你安装 eslint-plugin-react-hooks 插件,会得到下面的警告提示,提示我们使用 useEffect 缺少 count 的依赖。
React Hook useEffect has a missing dependency: 'count'. Either include it or remove the dependency array. You can also do a functional update 'setCount(c => ...)' if you only need 'count' in the 'setCount' call. (react-hooks/exhaustive-deps)
从上面的 eslint 插件中,我们可以快速修复这个问题。
useEffect(() => { setHanleCount({ action: () => setCount(count + 1), })}, [count])原因
在 React Fiber 架构中,Fiber 节点就是一个组件,其 Hook 节点类型大致如下:
type Hook = { memoizedState: any baseState: any baseUpdate: Update<any, any> | null queue: Update<any, any> | null next: Hook | null}memoizedState 用来存放组件的 state,next 指向 Hook 对象。由于不知道开发者会使用多少个 Hook,所以 Hook 都是链表存储的来保证调用的顺序。
假设我们有如下的代码,则对应的链表为:
function App() { const [] = useState() const [] = useState() useEffect(() => {}, []) useMemo(() => {}, []) const [] = useState()
return <></>}
当我们每次渲染更新的时候,react 会对 useEffect 和 useMemo 的依赖项进行判断是否改变,有更新会执行回调函数,否则跳过。
回到原先的问题上,react 进行组件渲染调用组件函数,函数执行创建了函数作用域。组件在初次渲染时,count 为 0,点击之后组件重新渲染,创建新的作用域,但是 useEffect 的依赖项未更新,其回调函数被跳过执行,里面的 count 变量引用的还是组件初始化的值。
所以 hooks 产生闭包陷阱的原因是依赖项缺失,在渲染过程中 hooks 的依赖项未更新跳过,内部函数依赖也是未更新前的值。
其他例子
在下面例子中,先点击 alert 按钮,后面点击 add 按钮,那么弹窗的值和页面展示的 value 是什么?
const FunctionComponent = () => { const [value, setValue] = useState(1)
const log = () => { setTimeout(() => { alert(value) }, 3000) }
return ( <div> <div>value: {value}</div> <button onClick={log}>alert</button> <button onClick={() => setValue(value + 1)}>add</button> </div> )}答案是:弹出的值是 1,页面的值是 2
我们发现弹出的值和当前页面显示的值不相同,这个就是一个闭包陷阱。
- 初次渲染,生成一个 log 函数(value = 1)
- 点击 alert 按钮,执行 log 函数(value = 1)
- 点击 add 按钮,value 的值增加,组件重新渲染,生成一个新的 log 函数(value = 2)
- 计时器触发,log 函数(value = 1)弹出闭包内的 value
使用 useRef 解决闭包陷阱
使用 useRef 每次渲染是都返回同一个引用类型的对象,读取和设置 value 都在这个对象上处理。
const FunctionComponent = () => { const [value, setValue] = useState(1)
const countRef = useRef(value)
const log = () => { setTimeout(() => { alert(countRef.current) }, 3000) }
useEffect(() => { countRef.current = value }, [value])
return ( <div> <div>value: {value}</div> <button onClick={log}>alert</button> <button onClick={() => setValue(value + 1)}>add</button> </div> )}