Зачем отказываться от использования useCallback (минусы useCallback)

внешний интерфейс React.js

Прежде всего, позвольте мне резюмировать мою точку зрения: за исключением очень немногих особых случаев, его не следует использовать.useCallback.

Если у вас есть разные мнения или сомнения по поводу этого моего вывода, поверьте мне, читайте дальше, следующий контент заставит ваше понимание его перейти на более высокий уровень.

НижеuseCallbackОсновное использование:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

В приведенном выше кодеmemoizedCallbackОн будет сгенерирован один раз в начале, а в дальнейшем процессе только его зависимостиaилиbОн будет восстановлен, если он изменится.

ПонялuseCallbackОсновное использование, мы используемuseCallbackОбернутая функция и функция, не обернутая ею, объединяются для сравнения:

function App() {
  const method1 = () => { 
    // ...
  }

  const  method2 = useCallback(() => {
      // 这是一个和 method1 功能一样的方法
  }, [props.a, props.b])

  return (
    <div>
      <div onClick={method1}>button</div>
      <div onClick={method2}>button</div>
    </div>
  )
}

Простите, в отличие от вышесказанного, этоmethod1производительность хорошая илиmethod2производительность хорошая?

Я слышал тебя, конечноmethod2Ах!

нашAppФункция перевыполняется каждый раз при обновлении, поэтому функции внутри нее также перегенерируются один раз.method1Каждое поколение заново исполнялось.

а такжеmethod2Это другое, это возвращаемое значение, обернутое useCallback, если зависимость не изменится, оно не будет перегенерировано, поэтому вы можете подумать, чтоmethod2Такой способ написания более эффективен.

Но на самом деле мы считаем это несколько неправильным.

Прежде всего, каждый раз, когда функция выполняется, переменные внутри нее перегенерируются, а накладные расходы незначительны.Это официальный сайт.Hooks FAQНаши соответствующие выводы приведены:

未命名.png

Даже если «переменные регенерируются каждый раз, компонент выполнен», не стоит игнорировать, использоватьuseCallbackТо же самое каждый раз, когда генерируется новая функция, но место, где она генерируется, очень скрыто, но она генерируется, а не используется. Теперь давайте посмотрим на это поближе.

const method1 = () => { }
const method2 = useCallback(() => {
        /* 一个和 method1 一样的方法 */
    }, 
    [props.a, props.b]
)

Предполагая, что сейчас он находится в фазе обновления, выполнитеmethod1, нам просто нужно применить и сохранитьmethod1Памяти, требуемой функцией, соответствующей этой переменной, вполне достаточно.

но выполнитьmethod2Шерстяная ткань,

  1. Во-первых, мы должны дополнительно выполнитьuseCallbackфункция,
  2. В то же время, мы также должны применитьuseCallbckПамять, требуемая функцией, соответствующей первому параметру, стоимость такая же, как иmethod1Накладные расходы одинаковы, даже если мы будем использовать кеш,useCallbackТакже требуются накладные расходы памяти для первого параметра.
  3. Кроме того, чтобы судитьuseCallbackВы хотите обновить результаты, мы должны полагаться на память, чтобы сэкономить время.
  4. и если нашuseCallbackВозвращаемая функция зависит от других значений компонента, и из-за особенностей замыканий в JS они всегда будут существовать и не уничтожаться.
const list = [...]
const method = useCallback(() => {
         console.log(list) // list 的引用会一直存在
    }, 
)

Глядя на это таким образом, используйтеuseCallback, который ничем не лучше оригинала.

мы проходимuseCallbackИсходный код еще раз подтверждает это:

function updateCallback<T>(
    callback: T, // useCallback 的第一个参数
    deps: Array<mixed> | void | null // useCallback 的第二个参数
): T {

  // 取到当前的 useCallback 语句对应的 hook 节点,
  const hook = updateWorkInProgressHook();
  
  // 当前的依赖,后面拿来和上一次的依赖进行比较
  const nextDeps = deps === undefined ? null : deps;
  
  // 取到上一次缓存的函数
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // 传了 useCallbck 的第二个参数才走到这里
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 上一次的依赖和这一次的依赖进行比较,
      // 相同就直接返回缓存的结果
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

Я считаю, что когда вы увидите это, вы поймете, почему вы не можете легко его использовать.useCallbckправильно?

Надо сказать, что для него слишком мало правильных сценариев использования.

Есть очень типичныйuseCallbckНеправильное использование сцены, стыдно сказать, я так написал. если мы последуемэтот документОписание добавляет конфигурацию ESLINT в наш проект, и при написании кода будет сообщено ошибка, аналогичный следующему:

export default function App() {
    const [count, setCount] = useState();
    const fetchApi = async () => {
        await fetch('https://jsonplaceholder.typicode.com/posts/1');
        console.log(count);
    };

    useEffect(() => {
        fetchApi();
    }, []);


    return <div>Hello World</div>;
}

image.png

Я не знаю, сколько людей столкнулись с подобными ошибками. Но мы знаем, что мы не можемfetchApiЭта функция добавлена ​​в зависимость.

Для этой проблемы самое простое и прямое решение — переместить функцию вuseEffectв.

Выполнение этого заставит некоторых людей чувствовать себя некомфортно, особенно студентов, которые только что пришли из компонента «Класс» (причина темы статьи, мы не будем на этом распространяться). Фактически,useEffectСама концепция дизайна рекомендует, чтобы мы поставили его внутрь, мы должны попытаться адаптировать к нему. Если вы привыкли, это будет очень хорошо.

Однако должны быть случаи, когда его нельзя разместить внутри, поэтому можно использовать следующие решения:

image.png

Скриншот выше взят из документацииБезопасно ли исключать функции из списка зависимостей?

Пожалуйста, обратите внимание на третий пункт~ Он также говорит, используйтеuseCallbackЭтот метод на самом деле является последним средством.После нашего предыдущего анализа вы должны лучше понимать причину, по которой он так сказал.

теперь, когдаuseCallbackТак плохо, когда это будет доступно?

Предположим, у нас естьCounterДочерний компонент , потребляет много денег при инициализации рендеринга:

<ExpensiveCounter count={count} onClick={handleClick} />

Если мы не будем проводить оптимизацию, любое обновление родительского компонента будет перерисовываться.Counter. Чтобы избежать повторного рендеринга дочернего компонента каждый раз, когда рендерится родительский компонент, мы можем использоватьReact.memo:

const ExpensiveCounter = React.memo(function Counter(props) {
    ...
})

использоватьReact.memoПосле упаковки,Counterкомпоненты толькоpropsОн будет перерисовываться только при изменении нашегоCounterпринять дваprops:Исходное значениеcount, функцияhandleClick.

Если родительский компонент из-за изменения других значений и обновления происходит, то родительский компонент будет перерендерен, т.к.handleClickЯвляется объектом, генерируется каждый рендерингhandleClickвсе новые.

Это приводит, хотяCounterодеялоReact.memoОберните слой, но он все равно будет перерендерен.Чтобы решить эту проблему, мы должны написать такhandleClickФункция:

const handleClick = useCallback(() => {
    // 原来的 handleClick...
}, [])

Таким образом, каждый раз, когда мы переходим кCounterкомпонентhandleClickвсе таки нашCounterкомпоненты толькоcountОн будет отображаться только тогда, когда произойдет изменение, а это именно то, что нам нужно, и это также играет хорошую роль в оптимизации.

Вышеупомянутая сцена может бытьuseCallbackОдин из немногих, очень подходящих для сцены. Но какой подкомпонент, с которым вы сталкиваетесь при выполнении задания Спецвыпуска, потребляет больше, чем вы? Во всяком случае, я столкнулся со многим.

На этой неделе планировал обновить статью про интерпретацию планировщика в React, но не знаю зачем, и никак не могу поднять мотивацию, не хочу читать исходники, может быть быть мотивированным на следующей неделе.

Да, кстати, в эти выходные погода очень холодная, и, увидев здесь друзей, я должна не забыть завтра одеться побольше.