具有依赖关系的 useCallback 与使用 ref 调用函数的最后一个版本
Posted
技术标签:
【中文标题】具有依赖关系的 useCallback 与使用 ref 调用函数的最后一个版本【英文标题】:useCallback with dependency vs using a ref to call the last version of the function 【发布时间】:2021-01-20 18:51:42 【问题描述】:在进行代码审查时,我遇到了这个自定义钩子:
import useRef, useEffect, useCallback from 'react'
export default function useLastVersion (func)
const ref = useRef()
useEffect(() =>
ref.current = func
, [func])
return useCallback((...args) =>
return ref.current(...args)
, [])
这个钩子是这样使用的:
const f = useLastVersion(() => // do stuff and depends on props )
基本上,与const f = useCallBack(() => // do stuff , [dep1, dep2])
相比,这避免了声明依赖项列表,并且f
永远不会改变,即使其中一个依赖项发生了变化。
我不知道如何看待这段代码。我不明白使用useLastVersion
与useCallback
相比有什么缺点。
【问题讨论】:
你可以直接回ref.current
这太多余了,这个useCallback
没用,你能问为什么它在那里吗?
@DennisVash 返回 ref.current
是不一样的:这里我们返回一个永远不会改变的函数,而 ref.current
会改变。
我的问题是为什么有一个函数,ref 对象本身(返回 ref)和函数有相同的生命周期,为什么还要有另一个包装器?
只使用 useCallback 似乎和你建议的一样
@DennisVash 不,它没有。直接使用函数时,每次渲染都会创建一个新函数,如果它作为 props 传递,子组件也会重新渲染。当使用useCallback
时,如果依赖数组没有改变,引用也不会改变,因此我们避免了子组件的潜在重新渲染。使用这个 useLastVersion
钩子,我们将其推得更远,因为我们进行了变异以避免任何重新渲染。
【参考方案1】:
这个问题实际上已经或多或少地在文档中得到了回答:https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
有趣的是:
另请注意,此模式可能会导致并发模式出现问题。我们计划在未来提供更符合人体工程学的替代方案,但目前最安全的解决方案是,如果某个值依赖于更改,则始终使回调无效。
另请阅读:https://github.com/facebook/react/issues/14099 和 https://github.com/reactjs/rfcs/issues/83
当前的建议是使用提供者来避免在 props 中传递回调,如果我们担心会导致过多的重新渲染。
【讨论】:
【参考方案2】:我的观点如 cmets 中所述,当依赖项更改过于频繁时(在 useEffect
/useCallback
dep 数组中),这个钩子在“你得到多少渲染”方面是多余的,使用普通函数是最好的选择(没有开销)。
这个钩子隐藏了使用它的组件的渲染,但渲染来自其父级中的useEffect
。
如果我们总结我们得到的渲染计数:
Ref + useCallback(钩子):在Component
中渲染(由于value
)+在钩子中渲染(useEffect
),一共2个。
仅使用回调:在Component
中渲染(由于value
)+ 在Counter
中渲染(在函数引用二重奏中更改为value
更改),共2 个。
正常功能:在Component
中渲染 + 在Counter
中渲染:每次渲染新功能,共2个。
但是在useEffect
或useCallback
中进行浅层比较会产生额外的开销。
实际例子:
function App()
const [value, setValue] = useState("");
return (
<div>
<input
value=value
onChange=(e) => setValue(e.target.value)
type="text"
/>
<Component value=value />
</div>
);
function useLastVersion(func)
const ref = useRef();
useEffect(() =>
ref.current = func;
console.log("useEffect called in ref+callback");
, [func]);
return useCallback((...args) =>
return ref.current(...args);
, []);
function Component( value )
const f1 = useLastVersion(() =>
alert(value.length);
);
const f2 = useCallback(() =>
alert(value.length);
, [value]);
const f3 = () =>
alert(value.length);
;
return (
<div>
Ref and useCallback:" "
<MemoCounter callBack=f1 msg="ref and useCallback" />
Callback only: <MemoCounter callBack=f2 msg="callback only" />
Normal: <MemoCounter callBack=f3 msg="normal" />
</div>
);
function Counter( callBack, msg )
console.log(msg);
return <button onClick=callBack>Click Me</button>;
const MemoCounter = React.memo(Counter);
附带说明,如果目的只是找到具有最少渲染的input
的长度,则阅读inputRef.current.value
将是解决方案。
【讨论】:
当然,这个例子我们或许可以做得更好,只是举个例子而已。如您所见,使用useLastVersion
,而不是重新渲染Counter
(想象一下,这是一个很重的渲染组件),我们只执行了一个非常轻的效果。以上是关于具有依赖关系的 useCallback 与使用 ref 调用函数的最后一个版本的主要内容,如果未能解决你的问题,请参考以下文章
React.memo()、useCallback()、useMemo() 区别及基本使用