Откройте React Hooks с анимацией и реальным боем (2): Custom Hook и useCallback

React.js
Откройте React Hooks с анимацией и реальным боем (2): Custom Hook и useCallback

Эта статья была написана членами сообщества Tuque.mRcнаписано, присоединяйтесьСообщество Туке, чтобы вместе создавать замечательные бесплатные технические руководства, чтобы способствовать развитию индустрии программирования.

Если вы считаете, что мы хорошо поработали, помнитеНравится + Подписаться + КомментарийСанлиан, поощряй нас писать лучшие уроки 💪

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

Добро пожаловать в этот проектРепозиторий GitHubа такжеGite-репозиторий.

Пользовательские крючки: индивидуальные

существуетПредыдущий урок, мы погружаемся все глубже и глубже через анимациюuseStateа такжеuseEffect, который в основном разъясняет механизм реализации React Hooks —связанный список, а также реализует глобальный обзор данных приложения визуализации данных COVID-19 и гистограммы данных по нескольким странам.

Если вы хотите начать читать и практиковаться непосредственно с этого руководства, вы можете загрузить исходный код этого руководства:

git clone -b second-part https://github.com/tuture-dev/covid-19-with-hooks.git
# 或者克隆 Gitee 的仓库
git clone -b second-part https://gitee.com/tuture/covid-19-with-hooks.git

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

Простой пользовательский хук

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

function useBodyScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(null);

  useEffect(() => {
    const handleScroll = () => setScrollPosition(window.scrollY);
    document.addEventListener('scroll', handleScroll);
    return () =>
      document.removeEventListener('scroll', handleScroll);
  }, []);

  return scrollPosition;
}

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

  • На первый взгляд: формат именованияuseXXXфункции, но не функциональные компоненты React
  • По существу: Внутренне, используя некоторые хуки, которые поставляются с React (например,useStateа такжеuseEffect) для реализации некоторой общей логики

Если вы мыслите нестандартно, вы можете придумать множество мест для создания пользовательских хуков: модификация/прослушивание побочных эффектов DOM, анимация, запросы, операции с формами, хранилище данных и т. д.

намекать

Здесь рекомендуются две мощные библиотеки React Hooks:React Useа такжеUmi Hooks. Все они реализуют множество пользовательских хуков производственного уровня, и их стоит изучить.

Думаю, в этом самая большая прелесть React Hooks — через несколько встроенных хуков можно составить любую комбинацию по определенным условностям, «создать» любой хук, который вам действительно нужен, или вызвать хук, написанный другими, чтобы легко справиться с с различными сложными бизнес-сценариями. Как будто огромный мир полон чудес, но состоит он всего из более чем ста элементов.

Взгляд на принципы, лежащие в основе пользовательских хуков

Снова пришло время анимации. Давайте посмотрим, что происходит при первом рендеринге компонента:

мы вAppкомпонент называетсяuseCustomHookкрюк. можно увидеть,Даже если мы переключимся на пользовательский хук, генерация связанного списка хуков все равно не изменится.. Давайте посмотрим на ситуацию повторного рендеринга:

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

Давайте еще раз вернемся к Правилам Крюка. Он предусматривает, что React Hooks можно использовать только в двух местах:

  1. Компоненты функции React
  2. Пользовательский крючок

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

реальный бой

Давайте продолжим разработку приложений для работы с данными о COVID-19. Далее мы намерены реализовать отображение исторических данных, включая подтвержденные случаи, смерти и излечения.

Давайте сначала реализуем пользовательский хук с именемuseCoronaAPI, который разделяет логику получения данных из API NovelCOVID 19. Создайтеsrc/hooks/useCoronaAPI.js, введите код следующим образом:

import { useState, useEffect } from "react";

const BASE_URL = "https://corona.lmao.ninja/v2";

export function useCoronaAPI(
  path,
  { initialData = null, converter = (data) => data, refetchInterval = null }
) {
  const [data, setData] = useState(initialData);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${BASE_URL}${path}`);
      const data = await response.json();
      setData(converter(data));
    };
    fetchData();

    if (refetchInterval) {
      const intervalId = setInterval(fetchData, refetchInterval);
      return () => clearInterval(intervalId);
    }
  }, [converter, path, refetchInterval]);

  return data;
}

Видно, что определеноuseCoronaAPIСодержит два параметра, первыйpath, который является путем к API; второй — это параметры конфигурации, включая следующие параметры:

  • initialData: данные по умолчанию, которые изначально пусты
  • converter: Функция преобразования исходных данных (по умолчанию используется функция идентификации).
  • refetchInterval: Интервал обновления данных в миллисекундах.

Кроме того, мы также должны обратить внимание наuseEffectвходящийdepsМассив, состоящий из трех элементов (все используются в функции Эффект):converter,pathа такжеrefetchInterval, оба изuseCoronaAPIВходящие параметры.

намекать

существуетпредыдущий пост, мы кратко упомянули, неuseEffectЗависимости лежат, тогда вот хороший случай: ставим функцию ЭффектИспользуются все внешние данные(включая функции) добавляются в массив зависимостей. Конечно, посколькуBASE_URLКонстанты, которые находятся на уровне модуля, поэтому не должны быть зависимостями.Но здесь осталась дыра, хе-хе...

затем в корневом компонентеsrc/App.jsиспользуя только что созданныйuseCoronaAPIХук, код такой:

import React, { useState } from "react";

// ...
import { useCoronaAPI } from "./hooks/useCoronaAPI";

function App() {
  const globalStats = useCoronaAPI("/all", {
    initialData: {},
    refetchInterval: 5000,
  });

  const [key, setKey] = useState("cases");
  const countries = useCoronaAPI(`/countries?sort=${key}`, {
    initialData: [],
    converter: (data) => data.slice(0, 10),
  });

  return (
    // ...
  );
}

export default App;

весьAppКомпоненты стали намного чище, не так ли?

Но когда мы запустили приложение с большими ожиданиями, то обнаружили, что все приложение попало в замкнутый круг «неограниченных запросов». Откройте вкладку «Сеть» инструментов разработчика Chrome, и вы обнаружите, что количество сетевых запросов постоянно растет...

Мы так испугались, что быстро закрыли страницу. Успокоившись, я не могу не думать: почему это?

Опасность

NovelCOVID 19 APIДля ресурсов данных общедоступного характера мы должны закрыть страницу как можно скорее, чтобы избежать чрезмерного давления запросов на сервер другой стороны.

useОбратный звонок: Dinghaishenzhen

Если вы сформулируете это слово в словопредыдущий постГлядя на него, на самом деле, вы, возможно, нашли подсказки к проблеме:

Зависимые массивы используются при оценке того, изменился ли элементObject.isсравнивать, так что когдаdepsКогда элемент не является примитивным типом (например, функция, объект и т. д.),меняется каждый рендер, тем самым вызывая Эффект каждый раз, теряяdepsсвой собственный смысл.

Хорошо, не беда, если у вас нет впечатлений, давайте поговорим о проблеме, с которой новички часто сталкиваются с React Hooks: Эффект бесконечного цикла.

Об эффекте бесконечного цикла

Взгляните на этот счетчик «никогда не останавливайтесь»:

function EndlessCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => setCount(count + 1), 1000);
  });

  return (
    <div className="App">
      <h1>{count}</h1>
    </div>
  );
}

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

Наш компонент застрял в:Рендеринг => триггер Эффект => изменение состояния => триггер повторного рендерингабесконечная петля.

Вы, должно быть, обнаружилиuseEffect«Виновник» попадания в бесконечный цикл — потому что правильныйdeps! Это приводит к тому, что функция Effect будет выполняться после каждого рендеринга. В самом деле, передuseCoronaAPI, а также потому, что входящийdepsЕсть проблема, из-за которой функция Effect выполняется для получения данных после каждого рендера и попадает в бесконечный цикл. Итак, какая зависимость является проблемой?

Это верно, это такconverterфункция!我们知道,在 JavaScript 中,原始类型和非原始类型在判断值是否相同的时候有巨大的差别:

// 原始类型
true === true // true
1 === 1 // true
'a' === 'a' // true

// 非原始类型
{} === {} // false
[] === [] // false
() => {} === () => {} // false

Точно так же каждый входящийconverterФункции, хотя формально и одинаковые, все же являются разными функциями (ссылки не равны), из-за чего каждый раз выполняется функция Эффект.

О мемоизации

Мемоизация, широко известная как кэш мемоизации (или «воспоминания»), звучит как глубокий компьютерный термин, но идея, стоящая за ней, проста: если у нас естьвычислительно дорогие чистые функции(Учитывая тот же ввод, мы обязательно получим тот же вывод), затем, когда мы сталкиваемся с определенным вводом в первый раз, мы «запоминаем» (кешируем) его результат вывода, затем в следующий раз, когда мы сталкиваемся с тем же выводом, Вы только нужно вынуть из кеша и вернуть напрямую, сохранив процесс расчета!

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

Давайте сначала ознакомимся с принципом мемоизации через простую функцию нахождения квадратного корня. Первая — это некэшированная версия:

function sqrt(arg) {
  return { result: Math.sqrt(arg) };
}

Вы могли заметить, что мы специально вернули объект для записи результатов, позже мы сравним и проанализируем версию Memoized. Затем есть кешированная версия:

function memoizedSqrt(arg) {
  // 如果 cache 不存在,则初始化一个空对象
  if (!memoizedSqrt.cache) {
    memoizedSqrt.cache = {};
  }

  // 如果 cache 没有命中,则先计算,再存入 cache,然后返回结果
  if (!memoizedSqrt.cache[arg]) {
    return memoizedSqrt.cache[arg] = { result: Math.sqrt(arg) };
  }

  // 直接返回 cache 内的结果,无需计算
  return memoizedSqrt.cache[arg];
}

Затем мы пытаемся вызвать эти две функции и обнаруживаем некоторые очевидные различия:

sqrt(9)                      // { result: 3 }
sqrt(9) === sqrt(9)          // false
Object.is(sqrt(9), sqrt(9))  // false

memoizedSqrt(9)                              // { result: 3 }
memoizedSqrt(9) === memoizedSqrt(9)          // true
Object.is(memoizedSqrt(9), memoizedSqrt(9))  // true

обычныйsqrtСсылка на возвращаемый результат каждый раз разная (илисовершенно новыйобъект), при этомmemoizedSqrtвозвращает точно такой же объект. Таким образом, в React мемоизация может гарантировать, что ссылки Prop или состояния в нескольких рендерингах равны, тем самым избегая ненужного повторного рендеринга или выполнения побочных эффектов.

Подытожим два сценария использования Memoization:

  • Экономьте время вычислений, кэшируя результаты вычислений.
  • Гарантированное ссылочное равенство для возвращаемых значений с одинаковыми входными данными

Используйте метод и принцип анализа

Чтобы решить функцию в нескольких рендерингахссылочное равенство(Референтное равенство), React представил важный хук —useCallback. Официальная документация описывает использование следующим образом:

const memoizedCallback = useCallback(callback, deps);

первый параметрcallbackИменно эту функцию нужно запомнить Второй параметр всем знаком.depsпараметр, а также массив зависимостей (иногда называемый входнымinputs). В контексте мемоизации этоdepsФункция эквивалентна ключу (Key) в кеше. Если ключ не изменился, то напрямую вернуть функцию в кеше и убедиться, что ссылка на ту же функцию.

В большинстве случаев мы передаем пустые массивы[]так какdepsпараметры, поэтомуuseCallbackвернутьВсегда одна и та же функция, никогда не обновляется.

намекать

Возможно, вы только начинаете учитьсяuseEffectкогда мы обнаруживаем, что нам не нужноuseStateВторая функция Setter возвращается как зависимость от Effect. На самом деле, React уже выполнил Memoization для функции Setter внутри, поэтому функция Setter, полученная из каждого рендеринга, абсолютно одинакова.depsДобавлено оно или нет, не имеет значения.

Как обычно, мы по-прежнему используем анимацию, чтобы понятьuseCallbackПринцип (deps— пустой массив), первый — это первый рендеринг:

Как и прежде, звонитеuseCallbackОн также добавляется к связанному списку хуков, но здесь эта функция подчеркивается.f1Ячейка памяти указывала (нарисована случайным образом), таким образом явно сообщая нам:этоf1всегда относится к одной и той же функции.然后返回的onClickОн указывает на хранящийся в Hookf1.

Давайте посмотрим на ситуацию повторного рендеринга:

При повторном рендеринге вызывать сноваuseCallbackтакже вернуться к намf1функция, и эта функция по-прежнему указывает на тот же участок памяти, так чтоonClickфункция и последний рендер это действительно сделалссылочное равенство.

Связь между useCallback и useMemo

мы знаемuseCallbackУ меня есть хороший другuseMemo. Помните, ранее мы суммировали два основных сценария мемоизации?useCallbackВ основном для решения функций"ссылочное равенство«Проблема иuseMemoэто"разносторонний человек", способный выполнять как ссылочное равенство, так и задачи по экономии вычислений.

Фактически,useMemoФункцияuseCallbackизсуперсет. а такжеuseCallbackможно сравнить только с кэшированными функциями,useMemoЛюбой тип значения (включая функции, конечно) может быть кэширован.useMemoиспользуется следующим образом:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Первый параметр — это функция, которая возвращает возвращаемое значение значения (то есть указанное вышеcomputeExpensiveValueрезультат) будет возвращен вmemoizedValue. Таким образом, следующие два использования ловушек полностью эквивалентны:

useCallback(fn, deps);
useMemo(() => fn, deps);

Учитывая, что задач, требующих больших вычислительных ресурсов, с которыми приходится сталкиваться при фронтенд-разработке, относительно немного, а производительность движка браузера достаточно высока, в этой серии статей не будет подробностей.useMemoиспользование. Более того, уже освоилuseCallbackвы, вы уже должны знать, как использоватьuseMemoправильно?

реальный бой

знакомыйuseCallbackПосле этого приступаем к исправлениюuseCoronaAPIПроблема с крючком. Исправлятьsrc/hooks/useCoronaAPI, код показан ниже:

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

// ...

export function useCoronaAPI(
  // ...
) {
  const [data, setData] = useState(initialData);
  const convertData = useCallback(converter, []);

  useEffect(() => {
    const fetchData = async () => {
      // ...
      setData(convertData(data));
    };
    fetchData();

    // ...
  }, [convertData, path, refetchInterval]);

  return data;
}

Как видите, мы положилиconverterфункцияuseCallbackОберните это и назовите запоминаемую функцию какconvertData, а входящийdepsПараметр представляет собой пустой массив[], чтобы каждый рендер был одинаковым. тогда поставьuseEffectвсе вconverterФункция изменена в соответствии сconvertData.

Наконец, проект был запущен снова, и все вернулось на круги своя. В следующем уроке мы начнем продвигать проект визуализации данных о COVID-19 и отображать исторические данные (включая диагнозы, смерти и излечения) в виде графиков. Состояние данных становится все более и более сложным, как мы с этим справляемся? Следите за обновлениями.

Спойлер: реализуйте простой Redux с помощью useReducer + useContext!

использованная литература

Хотите узнать больше интересных практических технических руководств? ПриходитьСообщество ТукеМагазин вокруг.