【问题标题】:Why can't `useCallback` always return the same ref为什么`useCallback`不能总是返回相同的引用
【发布时间】:2021-04-29 14:00:26
【问题描述】:

我不明白为什么useCallback 每次更新一个部门时总是返回一个新的参考。这会导致React.memo() 可以避免的许多重新渲染。

useCallback 的这个实现有什么问题(如果有的话)?

export function useCallback(callback) {

    const callbackRef = useRef();

    callbackRef.current = callback;

    return useState(() =>
        (...args) => callbackRef.current(...args)
    )[0];

}

使用它而不是内置实现肯定会对性能产生显着的积极影响。

自己的结论:

没有理由不使用使用 ref 的实现,只要您了解其含义,即,正如@Bergy 所指出的,您不能存储回调以供稍后使用(例如在 setTimeout 之后),并期望回调具有与同步调用它相同的效果。
然而,在我看来,这是首选行为,所以没有缺点????。

【问题讨论】:

  • 我猜默认行为是在回调函数需要更改时保持重新渲染,但避免每次内联回调时重新渲染整个树(每次渲染都创建新的,不管部门)。
  • 如果你想要相同的引用,那么你应该使用没有依赖关系的useCallback(..., [])。如果它取决于某些状态,则每次更改时都必须获取新值。这就是为什么 useCallback 需要使用一个新函数。
  • 好问题。我建议删除 Typescript 和 eslint 部分,因为它们与问题无关。
  • @BeS 使用这个使用回调的实现而不是默认的,你会看到,它的工作原理也一样。

标签: javascript reactjs react-hooks usecallback


【解决方案1】:

useCallback 的这个实现有什么问题(如果有的话)?

我怀疑当有人存储对您的回调的引用以供以后使用时会产生意想不到的后果,因为它会改变它正在做的事情:

const { Fragment, useCallback, useState } = React;

function App() {
  const [value, setValue] = useState("");
  const printer = useCallback(() => value, [value]);
  return <div>
    <input type="text" value={value} onChange={e => setValue(e.currentTarget.value)} />
    <Example printer={printer} />
  </div>
}

function Example({printer}) {
  const [printerHistory, setHistory] = useState([]);
  return <Fragment>
    <ul>{
      printerHistory.map(printer => <li>{printer()}</li>)
    }</ul>
    <button onClick={e => setHistory([...printerHistory, printer])}>Store</button>
  </Fragment>
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://unpkg.com/react@16.14.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

(当然,在这个简化的演示中,printer 回调只不过是对 value 本身的无用闭包,但您可以想象一个更复杂的情况,即可以选择一个单独的历史条目并希望在回调中使用复杂的按需计算)

使用原生 useCallback,存储在 printerHistory 中的函数将是不同值的不同闭包,而在您的实现中,它们都是引用最新 useCallback 参数的相同函数,并且只打印每次调用的当前值。

【讨论】:

  • 这是一个很好的例子。谢谢你指出来。现在我想我明白为什么 React 团队没有将我的实现设为默认实现,并且我觉得自己使用它更安全。
  • 我暂时不会将其标记为已回答,因为有人告诉我这可能会对即将到来的 react 并发模式产生一些影响,我想了解更多相关信息。
  • @JosephGarrone 我是一名库作者,并且对已经可用的并发模式功能进行了很多研究。您的useMemo() 是不必要的,您可以始终将最新的callback 分配给ref.current 并放弃deps 参数。您的实现在并发模式下的行为与在传统模式下的行为相同。
  • @JosephGarrone 不,您的useMemo() 没有任何会导致多次渲染时出现不同行为的副作用。如果它在strict mode 中工作正常(确实如此),它在并发模式下也能正常工作。
  • @kca 在计算结果的道具中有一个对象“方法”只是一个例子,有一个执行副作用的回调将是另一个例子。一旦子组件打算保留多个不同版本的传递值以供以后使用,例如在关闭带有超时或网络请求的useEffect 时,未获取原始引用将导致不同的行为。
【解决方案2】:

useCallback 和 useMemo 的用例不同。 你说 useCallback 返回一个回调的记忆版本,只有当依赖项之一发生变化时才会改变。结果,它根据依赖关系的变化重新渲染组件。 但是 useMemo 只保存可变值。 通常,我们使用 useCallback 来处理事件处理程序。 例如,假设当您单击按钮时,会显示按钮单击的计数。 在这种情况下,点击事件是使用 useCallback 实现的。

 const [count, setCount] = useState(0);
 const handleClick = useCallback(() => {
   setCount(count + 1);
   console.log(count + 1);
 }, [count]);

要获得新的增加计数值并在单击按钮时显示其值,需要重新渲染。 再说一遍,useCallback 和 useMemo 的用例取决于用途。

【讨论】:

  • 感谢您抽出宝贵的时间来回答 :) 您错过了重点......
猜你喜欢
  • 2019-06-26
  • 2013-09-20
  • 1970-01-01
  • 1970-01-01
  • 2017-07-04
  • 2014-05-18
  • 2010-12-22
  • 2018-09-24
  • 1970-01-01
相关资源
最近更新 更多