具有依赖关系的 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 永远不会改变,即使其中一个依赖项发生了变化。

我不知道如何看待这段代码。我不明白使用useLastVersionuseCallback 相比有什么缺点。

【问题讨论】:

你可以直接回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个。

但是在useEffectuseCallback 中进行浅层比较会产生额外的开销。

实际例子:

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 调用函数的最后一个版本的主要内容,如果未能解决你的问题,请参考以下文章

从CRAN镜像安装具有依赖关系的本地R包

React.memo()、useCallback()、useMemo() 区别及基本使用

我应该在 useCallback 的依赖数组中包含 setState 吗?

useMemo和useCallback的区别和使用

功能依赖 - BCNF 规范化问题

React Hook useCallback 缺少依赖项:'Id'。包括它或删除依赖数组[重复]