React中回调函数访问旧memo状态的问题求助
我最近在学ReactJS的性能优化,碰到了一个完全搞不懂的情况,已经把代码精简到最小复现版本了,大家帮我看看这奇怪的行为是咋回事?
我的代码逻辑是这样的:
- 有一个输入框,用来修改状态
query - 用
useMemo生成了一个memoT,它会在query变化时更新 - 写了一个
callback函数,用来打印memoT的值 - 用
useCallback把这个callback包装成了testCall - 最后把
testCall绑定到按钮的点击事件上
下面是完整代码:
import { useState, useMemo, useCallback } from "react"; export function App(props) { const [query, setQuery] = useState(''); const memoT = useMemo(() => (query + '_memo'), [query]); function callback() { console.log("callback : "+memoT) } const testCall = useCallback(() => callback(), []); return ( <> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <p>{memoT}</p> <button onClick={testCall} >run testCall</button> </> ); }
我预期的行为:
点击按钮时,callback应该读取当前的memoT值,也就是输入框里的内容加上_memo,然后打印出来。
实际发生的情况:
每次点击按钮,callback里打印的memoT永远是初始值(空字符串加_memo,也就是_memo),但页面上<p>{memoT}</p>显示的内容是正确的,会跟着输入框实时更新。
我的猜测:
是不是useCallback创建的时候,就“记住”了当时的memoT引用?那我该怎么让回调函数能读取到最新的memoT值呢?
嘿,这个问题其实是React闭包特性搞的鬼,我来给你掰扯清楚:
首先你得明白React组件每次状态更新都会重新执行整个组件函数,而你用的useCallback依赖数组是空的[],这意味着它只会在组件第一次渲染时创建这个回调函数,并且永远不会更新。
第一次渲染时,memoT的初始值是''_memo(也就是_memo),这时候testCall这个回调函数(还有它调用的callback)就通过闭包“捕获”了当前这个初始的memoT值——闭包的特性就是会锁住函数创建时所在作用域的变量值,不管之后这个变量在外面怎么变,闭包里面的都是当时捕获的那个旧值。
那怎么解决呢?给你几个实用的办法:
办法1:把memoT加入useCallback的依赖数组
既然testCall需要用到最新的memoT,那直接把它放进依赖里就行:
const testCall = useCallback(() => callback(), [memoT]);
这样每次memoT更新时,useCallback就会重新创建一个新的回调函数,这个新函数会捕获最新的memoT值,点击按钮时就能打印正确内容了。
办法2:用useRef保存memoT的最新值
如果不想让useCallback频繁更新(比如依赖项很多的场景),可以用useRef来存最新的memoT——因为ref的current属性是可变的,不会被闭包锁住:
import { useState, useMemo, useCallback, useRef, useEffect } from "react"; export function App(props) { const [query, setQuery] = useState(''); const memoTRef = useRef(''); const memoT = useMemo(() => (query + '_memo'), [query]); // 每次memoT更新时,同步到ref里 useEffect(() => { memoTRef.current = memoT; }, [memoT]); function callback() { console.log("callback : "+memoTRef.current) } const testCall = useCallback(() => callback(), []); return ( <> <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <p>{memoT}</p> <button onClick={testCall} >run testCall</button> </> ); }
这样testCall的依赖还是空数组,但callback里读取的memoTRef.current永远是最新的memoT值。
办法3:直接把逻辑内联到useCallback里
其实你可以去掉中间的callback函数,把逻辑直接写到useCallback里,代码更简洁:
const testCall = useCallback(() => { console.log("callback : "+memoT) }, [memoT]);
本质和办法1一样,但少了一层函数嵌套,可读性更强。
总结一下:React的闭包特性会让函数捕获创建时所在作用域的变量值,如果你用useCallback时依赖数组没加对,就会拿到旧的变量值。解决的核心就是要么让useCallback跟着依赖更新,要么用ref存最新值绕过闭包的捕获。
备注:内容来源于stack exchange,提问作者Jeremy

