首页 > 解决方案 > React — 这是对 useCallback 钩子的好用吗?

问题描述

我试图了解使用useCallback钩子定义所有回调和/或事件处理程序的新趋势背后的基本原理。

据我了解onClick === Callback_A,只要x保持不变,它就是正确的:

const onClick = useCallback(() => 42, [x]);
//                          ^^^^^^^^
//                          Callback_A

然而,函数表达式(的第一个参数useCallback)在每次渲染后总是一个新的。只有x在没有改变的情况下才会被丢弃。

假设一个组件由于某种原因渲染了 3 次,但x始终保持不变:

/*********************************** 1st render ***/

const onClick = useCallback(() => 42, [x]);
//    ^^^^^^^               ^^^^^^^^
//    Callback_A            Callback_A

/*********************************** 2nd render ***/

const onClick = useCallback(() => 42, [x]);
//    ^^^^^^^               ^^^^^^^^
//    Callback_A            Callback_A'

/*********************************** 3rd render ***/

const onClick = useCallback(() => 42, [x]);
//    ^^^^^^^               ^^^^^^^^
//    Callback_A            Callback_A''

() => 42如果在每次渲染时创建一个新的函数表达式(即),无论x是否相同,为什么不直接将其分配给onClick呢?

当我在代码审查期间进行观察时,答案始终是"it-is-to-avoid-useless-re-rendering-of-child-components" 的变体。

我想对这个理论进行测试,并创建了一个带有五个按钮的简单应用程序:(
见下面的截屏视频)

我计算生成了多少次 onclick 处理程序以及每个按钮实际使用了多少个 onclick 处理程序:

import { useCallback, useEffect, useState } from "react";

const stats =
  { N: {}
  , B: {}
  , C: {}
  , D: {}
  , E: {} };

const register_handler = btn => {
  const now = performance.now();
  stats[btn][now] = false;
  return () => stats[btn][now] = true;
};

const print_stats = () => {
  console.table(
    Object.entries(stats).map(([button, handlers]) => ({
      button,
      count_generated: Object.keys(handlers).length,
      count_executed: Object.values(handlers).filter(b => b).length
    }))
  );
};

const e_click = register_handler('E');

export default function App() {
  // Only `n` will change. All other vars will remain the same.
  const [n, write_n] = useState(1);
  const [b] = useState('B');
  const [c] = useState('C');
  const [d] = useState('D');
  const [e] = useState('E');

  // Forces <App/> to re-render every two seconds.
  useEffect(() => setTimeout(() => write_n(n + 1), 2000));

  const n_click = useCallback(register_handler('N'), [n]);
  const b_click = useCallback(register_handler('B'), [b]);
  const c_click = register_handler('C');
  
  return (
    <fieldset>
      <legend>your buttons are belong to us</legend>
      <button value={n} onClick={n_click}>{n}</button>
      <button value={b} onClick={b_click}>{b}</button>
      <button value={c} onClick={c_click}>{c}</button>
      <button value={d} onClick={register_handler('D')}>{d}</button>
      <button value={e} onClick={e_click}>{e}</button>
      <hr />
      <button onClick={print_stats}>print stats</button>
    </fieldset>
  );
}

我准确地点击每个按钮三次,然后打印一些统计数据:

按钮 生成的处理程序数 使用的处理程序数量
ñ 24 3
24 1
C 24 3
D 24 3
1 1

观察:

  1. 除了按钮之外,E所有其他按钮都生成了太多的事件处理程序。
  2. 两个按钮都C没有D使用useCallback,但他们没有重新渲染。

除了按钮之外,E所有其他按钮都生成了相同数量的事件处理程序,但只消耗了一些。有些人自始至终重复使用相同的事件处理程序,而另一些人每次都使用一个新的事件处理程序。我们对缓存的好处useCallback超过它的成本有多大信心?我想任何涉及缓存的东西都会带来一些权衡。

这种无条件的爱useCallback也伴随着一些不平凡的维护成本。不是每个人都熟悉它,我怀疑它的某些使用是由某种程度的货物崇拜编程驱动的。

在这个特定的例子中,任何人都有理由使用useCallback,为什么?


https://codesandbox.io/s/experimenting-with-usecallback-c4jf1

在此处输入图像描述

标签: reactjsreact-hooks

解决方案


推荐阅读