Интенсивное чтение «Введение в функциональный компонент»

JavaScript React.js

1. Введение

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

2. Интенсивное чтение

Что такое функциональный компонент?

Функциональный компонент — это компонент React, созданный в виде функции:

function App() {
  return (
    <div>
      <p>App</p>
    </div>
  );
}

То есть тот, который возвращает JSX илиcreateElementФункцию можно рассматривать как компонент React, и эта форма компонента является функциональным компонентом.

Итак, я уже изучил функциональный компонент?

Не волнуйтесь, история только начинается.

Что такое крючки?

Хуки — это инструменты, помогающие функциональному компоненту. НапримерuseStateЭто хук, который можно использовать для управления состоянием:

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

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

Сначала назначьте, а затем установите печать Timeout

мы будемuseStateа такжеsetTimeoutОбъедините их и посмотрите, что вы найдете.

Создайте кнопку, которая увеличивает счетчик при нажатии,Но распечатывает с задержкой в ​​3 секунды:

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

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(count);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={log}>Click me</button>
    </div>
  );
}

если мыНажмите три раза подряд в течение трех секунд,ТакcountЗначение в конечном итоге станет3, и в результате получается . . ?

0
1
2

Что ж, это кажется правильным, но всегда кажется немного странным?

Как насчет использования Class Component для повторной реализации?

Стукнем по доске, вернемся в знакомый нам режим Class Component и снова реализуем вышеуказанные функции:

class Counter extends Component {
  state = { count: 0 };

  log = () => {
    this.setState({
      count: this.state.count + 1
    });
    setTimeout(() => {
      console.log(this.state.count);
    }, 3000);
  };

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.log}>Click me</button>
      </div>
    );
  }
}

Ну, результаты должны быть эквивалентны, верно? Нажмите кнопку три раза в течение 3 секунд, на этот раз результат является:

3
3
3

Почему результат отличается от функционального компонента?

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

Сначала объясните компонент класса:

  1. Первое состояние неизменно,setStateПосле этого будет сгенерирована новая ссылка на состояние.
  2. Но компонент класса проходитthis.stateспособ чтения состояния,Это приводит к тому, что последняя ссылка на состояние извлекается каждый раз при выполнении кода., поэтому результат трех быстрых щелчков3 3 3.

Затем для функционального компонента:

  1. useStateСгенерированные данные также являются неизменяемыми.После установки нового значения через второй параметр массива исходное значение сформирует новую ссылку при следующем рендеринге.
  2. Но так как чтение состояния не удаетсяthis.таким образом, чтокаждый разsetTimeoutОба считывают данные среды закрытия рендеринга в то время.Хотя последнее значение изменилось с последним рендерингом, в старом рендеринге состояние остается старым значением.

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

Первый клик, отрендеренный всего 2 раза,setTimeoutэффективным в1Второй рендеринг, состояние:

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

  const log = () => {
    setCount(0 + 1);
    setTimeout(() => {
      console.log(0);
    }, 3000);
  };

  return ...
}

Второй щелчок, отрендеренный всего 3 раза,setTimeoutэффективным в2Второй рендеринг, состояние:

function Counter() {
  const [1, setCount] = useState(0);

  const log = () => {
    setCount(1 + 1);
    setTimeout(() => {
      console.log(1);
    }, 3000);
  };

  return ...
}

Третий клик, отрендеренный всего 4 раза,setTimeoutэффективным в3Второй рендеринг, состояние:

function Counter() {
  const [2, setCount] = useState(0);

  const log = () => {
    setCount(2 + 1);
    setTimeout(() => {
      console.log(2);
    }, 3000);
  };

  return ...
}

Как видите, каждый рендеринг является независимым замыканием.countЗначения в каждом рендере0 1 2, так что всеsetTimeoutКак долго задержка, распечатанный результат всегда будет0 1 2.

Имея это в виду, мы можем двигаться дальше.

Как сделать так, чтобы функциональный компонент также печатался3 3 3?

Значит ли это, что функциональный компонент не может переопределить функцию классового компонента? совершенно нет,Я надеюсь, что прочитав эту статью, вы сможете не только решить эту проблему, но и понять, почему код, реализованный с помощью Function Component, более разумен и элегантен..

Первое решение — использовать новый хук —useRefСпособность:

function Counter() {
  const count = useRef(0);

  const log = () => {
    count.current++;
    setTimeout(() => {
      console.log(count.current);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button onClick={log}>Click me</button>
    </div>
  );
}

Результат печати этой схемы3 3 3.

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

По этому мыcount.currentНазначение или чтение, чтение всегда является его последним значением, независимо от закрытия рендеринга, поэтому, если вы быстро щелкните три раза, оно обязательно вернется3 3 3результат.

Но есть проблема с этой схемой, то есть с использованиемuseRefзамененыuseStateПри создании значения возникает естественный вопрос: как добиться того же эффекта, не меняя способ написания исходного значения?

Как также печатать без преобразования исходного значения3 3 3?

Самый простой способ сделать это — создать новыйuseRefЗначение даетsetTimeoutиспользовать, в то время как остальная часть программы использует оригиналcount:

function Counter() {
  const [count, setCount] = useState(0);
  const currentCount = useRef(count);

  useEffect(() => {
    currentCount.current = count;
  });

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={log}>Click me</button>
    </div>
  );
}

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

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

Мы можем воспользоваться этой функцией, после каждого рендерингаcountПоследнее значение в это время присваиваетсяcurrentCount.current, Таким образом делаяcurrentCountЗначение автоматически синхронизируется.countпоследнее значение .

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

Первый клик, отрендеренный всего 2 раза,useEffectэффективным в2Вторичный рендеринг:

function Counter() {
  const [1, setCount] = useState(0);
  const currentCount = useRef(0);

  useEffect(() => {
    currentCount.current = 1; // 第二次渲染完毕后执行一次
  });

  const log = () => {
    setCount(1 + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

  return ...
}

Второй щелчок, отрендеренный всего 3 раза,useEffectэффективным в3Вторичный рендер:

function Counter() {
  const [2, setCount] = useState(0);
  const currentCount = useRef(0);

  useEffect(() => {
    currentCount.current = 2; // 第三次渲染完毕后执行一次
  });

  const log = () => {
    setCount(2 + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

  return ...
}

Третий клик, отрендеренный всего 4 раза,useEffectэффективным в4Вторичный рендер:

function Counter() {
  const [3, setCount] = useState(0);
  const currentCount = useRef(0);

  useEffect(() => {
    currentCount.current = 3; // 第四次渲染完毕后执行一次
  });

  const log = () => {
    setCount(3 + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

  return ...
}

Обратите внимание на контраст сsetTimeoutКакая разница при рендеринге.

Чтобы быть осторожным,useEffectТакже зависит от каждого рендера,Между различными рендерингами одного и того же компонента,useEffectВнутренняя среда закрытия полностью независима. Для этого примераuseEffectвыполнено всегочетыре раза, после четырех назначений следующим образом, наконец, становится3:

currentCount.current = 0; // 第 1 次渲染
currentCount.current = 1; // 第 2 次渲染
currentCount.current = 2; // 第 3 次渲染
currentCount.current = 3; // 第 4 次渲染

Прежде чем читать дальше, убедитесь, что вы поняли это предложение:

  • setTimeoutнапример, три клика вызывают четыре рендеринга, ноsetTimeoutвступает в силу при 1-м, 2-м и 3-м рендеринге соответственно, поэтому значение равно0 1 2.
  • useEffectнапример, три щелчка также запускают четыре рендеринга, ноuseEffectВступают в силу в 1-й, 2-й, 3-й и 4-й визуализациях соответственно и, наконец, делаютcurrentCountзначение становится3.

Оберните его специальным крючкомuseRef

Вам не хочется писать их кучу каждый раз?useEffectСинхронизировать данные сuseRefраздраженный? Да, для упрощения нужно ввести новое понятие:Пользовательские крючки.

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

То есть мы можемuseEffectНапишите в пользовательский хук:

function useCurrentValue(value) {
  const ref = useRef(0);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref;
}

Вот новая концепция,то естьuseEffectВторой параметр, зависимости. зависимости Этот параметр определяетuseEffect, в новом рендере, пока ссылки всех зависимостей не меняются,useEffectне будет выполняться, а когда зависимость[]час,useEffectВыполняется только один раз при инициализации, последующие рендереры никогда не будут выполняться.

В этом примере мы говорим React: только еслиvalueЗначение , а затем синхронизируйте его последнее значение сref.current.

Затем этот пользовательский хук можно вызвать в любом функциональном компоненте:

function Counter() {
  const [count, setCount] = useState(0);
  const currentCount = useCurrentValue(count);

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={log}>Click me</button>
    </div>
  );
}

После инкапсуляции код намного чище, а самое главное инкапсулировать логику, надо только понятьuseCurrentValueЭтот хук может создать значение, последнее значение которого всегда синхронизировано с входным параметром.

Смотрите здесь, возможно, некоторые маленькие партнеры испытали порыв вдохновения:БудуuseEffectВторой параметр установлен в пустой массив, и этот пользовательский хук представляетdidMountЖизненный цикл!

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

БудуsetTimeoutзаменитьsetIntervalчто случится

Вернемся к исходной точке и поставим первыйsetTimeoutЗаменено в демо наsetInterval, и посмотрим, что произойдет:

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

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

Во-первых, давайте представим введенную новую концепцию,useEffectвозвращаемое значение функции. Его возвращаемое значение — это функция, которая находится вuseEffectКогда он собирается повторно выполняться, последний рендерер будет выполнен первым.useEffectФункция возврата первого обратного вызова, а затем выполнение следующего рендерингаuseEffectПервый обратный звонок.

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

Первый рендер:

function Counter() {
  useEffect(() => {
    // 第一次渲染完毕后执行
    // 最终执行顺序:1
    return () => {
      // 由于没有填写依赖项,所以第二次渲染 useEffect 会再次执行,在执行前,第一次渲染中这个地方的回调函数会首先被调用
      // 最终执行顺序:2
    }
  });

  return ...
}

Второй рендер:

function Counter() {
  useEffect(() => {
    // 第二次渲染完毕后执行
    // 最终执行顺序:3
    return () => {
      // 依此类推
    }
  });

  return ...
}

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

Прочитав предыдущий пример, вы должны подумать, что эта демонстрация надеется использовать[]зависит отuseEffectтак какdidMountиспользовать, комбинироватьsetIntervalкаждый разcountсамоприращение, так что ожидание будетcountЗначение увеличивается на 1 каждую секунду.

Однако результат таков:

1
1
1
...

пониматьsetTimeoutЧитатели примера должны понять, почему:setIntervalВсегда в закрытии первого рендера,countЗначение всегда0, что эквивалентно:

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(0 + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

Однако виновником являетсяне быть честным в отношении довериявызванный. примерuseEffectочевидно, зависело отcount, но зависимости должны быть записаны[], поэтому возникает непонятная ошибка.

Так что способ исправить эточестность о зависимости.

Навсегда зависимости честные

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

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);

  return <h1>{count}</h1>;
}

мы будемcountтак какuseEffectзависимости, вы получите правильный результат:

1
2
3
...

Так как риск отсутствия зависимостей настолько велик, естественно существуют защитные меры, т.е.eslint-plugin-react-hooksЭтот плагин будет автоматически пересматривать вашу зависимость в вашем коде, вы не можете этого сделать, вы не можете этого сделать!

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

Как не создавать повторный экземпляр при каждом рендереsetInterval?

Самый простой способ - использоватьuseStateИспользование второго присваивания , не зависит напрямую отcount, но присвойте значение в обратном вызове функции:

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

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

Это письмо действительно делает это:

  1. не зависеть отcount, так что будьте честны насчет зависимостей.
  2. Зависимость[], будет только инициализацияsetIntervalсоздать экземпляр.

И вывод по-прежнему правильный1 2 3 ..., Причина вsetCountв функции обратного вызова,cЗначение всегда указывает на последнююcountзначение, поэтому нет логических дыр.

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

При использовании более двух переменных одновременно?

Если вам нужноcountа такжеstepНакопить две переменные, затемuseEffectЗависимость должна быть записана с определенным значением, и снова возникает проблема частого инстанцирования:

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);

  return <h1>{count}</h1>;
}

В этом примере, посколькуsetCountполучить только последнююcountзначение, и для того, чтобы каждый раз получать последниеstepзначение, оно должно бытьstepобъявитьuseEffectзависимость, в результате чегоsetIntervalчасто создается экземпляр.

Эта проблема, естественно, беспокоила команду React, поэтому они придумали новый хук для решения проблемы:useReducer.

что такое useReducer

Пока не думайте о Redux. Просто рассмотрите приведенный выше сценарий и поймите, почему команда ReactuseReducerПеречислен как один из встроенных хуков.

Представьте это на это сначалаuseReducerПрименение:

const [state, dispatch] = useReducer(reducer, initialState);

useReducerВозвращаемая структура такая же, какuseStateПочти так же, за исключением того, что второй элемент в массивеdispatch, а полученных параметров тоже два, начальное значение ставится на второе место, первоеreducer.

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

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };
    default:
      return state;
  }
}

Это можно сделать, позвонивdispatch({ type: 'increment' })способ достиженияcountсамовозрастающий.

Итак, вернемся к этому примеру, нам просто нужно немного переписать использование:

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: "tick" });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return <h1>{count}</h1>;
}

function reducer(state, action) {
  switch (action.type) {
    case "tick":
      return {
        ...state,
        count: state.count + state.step
      };
  }
}

Видно, что мы проходимreducerизtickвведите полную паруcountнакопление , в то время как вuseEffectВ функции полностью игнорируетсяcount,stepэти две переменные. такuseReducerТакже известен как «черная магия» для решения таких проблем.

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

Внимательные читатели обнаружат, что в этом примере все еще есть зависимость, т.е.dispatch,ОднакоdispatchСсылка никогда не меняется, поэтому ее эффект можно игнорировать.Это также отражает честность в отношении зависимостей, несмотря ни на что.

Это также вызывает еще одно замечание:Попробуйте написать функцию вuseEffectвнутренний.

написать функцию вuseEffectвнутренний

Чтобы избежать пропущенных зависимостей, функция должна быть написана наuseEffectвнутри вот такeslint-plugin-react-hooksЧтобы завершить зависимости с помощью статического анализа:

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

  useEffect(() => {
    function getFetchUrl() {
      return "https://v?query=" + count;
    }

    getFetchUrl();
  }, [count]);

  return <h1>{count}</h1>;
}

getFetchUrlЭта функция зависит отcount, а если функция определена вuseEffectСнаружи трудно разглядеть машина это или человеческий глазuseEffectЗависимости содержатcount.

Однако при этом возникает новая проблема: запись всех функций вuseEffectНе сложно ли ухаживать за салоном?

как извлечь функциюuseEffectвнешний?

Чтобы решить эту проблему, мы собираемся представить новый хук:useCallback, это решение для извлечения функции вuseEffectвнешние проблемы.

давайте сначала посмотримuseCallbackПрименение:

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

  const getFetchUrl = useCallback(() => {
    return "https://v?query=" + count;
  }, [count]);

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

  return <h1>{count}</h1>;
}

можно увидеть,useCallbackТак же есть второй параметр - зависимости, будемgetFetchUrlЗависимости функции передаются черезuseCallbackпакет на новыйgetFetchUrlфункция, тоuseEffectпросто зависеть отgetFetchUrlЭта функция реализуетcountкосвенные зависимости.

Другими словами, мы используемuseCallbackБудуgetFetchUrlфункция подобранаuseEffectвнешний.

ЗачемuseCallbackСравниватьcomponentDidUpdateлучше использовать

Вспомните паттерн Class Component, как мы повторно получаем данные при изменении параметров функции:

class Parent extends Component {
  state = {
    count: 0,
    step: 0
  };
  fetchData = () => {
    const url =
      "https://v?query=" + this.state.count + "&step=" + this.state.step;
  };
  render() {
    return <Child fetchData={this.fetchData} count={count} step={step} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (
      this.props.count !== prevProps.count &&
      this.props.step !== prevProps.step // 别漏了!
    ) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

Вышеприведенный код должен быть знаком тем, кто часто использует Class Component, но выявленные проблемы не малы.

нам нужно понятьprops.count props.stepодеялоprops.fetchDataиспользуется функция, поэтому вcomponentDidUpdateКогда выясняется, что эти два параметра изменились, запускается повторная выборка.

Но вопрос в том, не слишком ли высока цена этого понимания? если родительская функцияfetchDataЭто не я написал, откуда я знаю, что это зависит от него, не читая исходный кодprops.countа такжеprops.stepШерстяная ткань?Серьезнее, если однаждыfetchDataполагаться наparamsЭтот параметр нижестоящей функции понадобится всем вcomponentDidUpdateпереопределить эту логику, иначеparamsне будет повторно загружаться при изменении.可以想象,这种方式维护成本巨大,甚至可以说几乎无法维护。

Функциональный компонент думать об этом! Попробуйте провести только что упомянутыеuseCallbackРешать проблему:

function Parent() {
  const [ count, setCount ] = useState(0);
  const [ step, setStep ] = useState(0);

  const fetchData = useCallback(() => {
    const url = 'https://v/search?query=' + count + "&step=" + step;
  }, [count, step])

  return (
    <Child fetchData={fetchData} />
  )
}

function Child(props) {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return (
    // ...
  )
}

Видно, что когдаfetchDataПосле изменения зависимости нажмите клавишу сохранения,eslint-plugin-react-hooksОбновленные зависимости будут добавлены автоматически,Нисходящий код не нужно вносить никаких изменений, нижестоящему нужно только заботиться о зависимостях.fetchDataЭтой функции достаточно.Что касается того, от чего зависит эта функция, то она инкапсулирована вuseCallbackЗатем его упаковали и передали.

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

useEffectАбстракция бизнеса очень удобна.Автор приводит несколько примеров:

  1. зависимости являются параметрами запроса, тогдаuseEffectЕсли параметр запроса изменится, список будет автоматически загружен и обновлен. Обратите внимание, что мы изменили время выборки со стороны триггера на сторону получателя.
  2. Когда список будет обновлен, повторно зарегистрируйте событие ответа на перетаскивание. Точно так же зависимым параметром является список.Пока список изменяется, реакция перетаскивания будет повторно инициализирована, так что мы можем уверенно изменять список, не беспокоясь о сбое события перетаскивания.
  3. Пока определенные данные в потоке данных изменяются, заголовок страницы изменяется синхронно. Точно так же нет необходимости изменять заголовок каждый раз при изменении данных, ноuseEffect"слушает" изменения в данных, что является"Инверсия контроля"мышление.

Сказав так много, его сущность все еще используетсяuseCallbackИзвлеките функцию независимо, чтобыuseEffectвнешний.

Тогда думай дальше,Можно ли абстрагировать функцию от всего компонента?

Это также возможно, но требует гибкого использования пользовательских реализаций хуков.

Извлечь функцию за пределы компонента

с вышеуказаннымfetchDataФункция в качестве примера, если вы хотите извлечь снаружи весь компонент, это не использоватьuseCallbackЭто делается, но для этого используются пользовательские хуки:

function useFetch(count, step) {
  return useCallback(() => {
    const url = "https://v/search?query=" + count + "&step=" + step;
  }, [count, step]);
}

Как видите, мы будемuseCallbackУпаковано и перенесено на кастомный крючокuseFetch, то для достижения того же эффекта в функции требуется только одна строка кода:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const [other, setOther] = useState(0);
  const fetch = useFetch(count, step); // 封装了 useFetch

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>setCount {count}</button>
      <button onClick={() => setStep(c => c + 1)}>setStep {step}</button>
      <button onClick={() => setOther(c => c + 1)}>setOther {other}</button>
    </div>
  );
}

По мере того, как пользоваться им станет удобнее, мы можем сосредоточиться на производительности. Наблюдение показывает, что,countа такжеstepчасто меняется, и каждое изменение приводит кuseFetchсерединаuseCallbackИзменения зависимостей, которые, в свою очередь, вызывают перегенерацию функции. Однако на самом деле нет необходимости перегенерировать такую ​​функцию каждый раз, повторная генерация функции приведет к большим потерям производительности.

Другой пример можно увидеть более четко:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const [other, setOther] = useState(0);
  const drag = useDraggable(count, step); // 封装了拖拽函数
}

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

function useDraggable(count, step) {
  return useCallback(() => {
    // 上报日志
    report(count, step);

    // 对区域进行初始化,非常耗时
    // ... 省略耗时代码
  }, [count, step]);
}

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

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

Один из способов — преобразовать зависимость в Ref:

function useFetch(count, step) {
  const countRef = useRef(count);
  const stepRef = useRef(step);

  useEffect(() => {
    countRef.current = count;
    stepRef.current = step;
  });

  return useCallback(() => {
    const url =
      "https://v/search?query=" + countRef.current + "&step=" + stepRef.current;
  }, [countRef, stepRef]); // 依赖不会变,却能每次拿到最新的值
}

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

Однако это относительно много для функции функции, и существует более распространенная практика решения таких задач.

Универсальные пользовательские хуки решают проблему повторного создания экземпляра функции

мы можем использоватьuseRefВместо этого создайте собственный хукuseCallback,Когда значение, от которого он зависит, изменяется, обратный вызов не будет выполняться повторно, но получит самое последнее значение!

Этот волшебный крючок записывается следующим образом:

function useEventCallback(fn, dependencies) {
  const ref = useRef(null);

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

Еще раз ощутите всемогущество кастомных хуков.

Сначала посмотрите на этот абзац:

useEffect(() => {
  ref.current = fn;
}, [fn, ...dependencies]);

когдаfnКогда функция обратного вызова изменяется,ref.currentПовторить на последнююfnЭта логика вполне удовлетворительна. Дело в том, что, опираясь наdependenciesизменить, а также повторноref.currentназначить, в это времяfnВнутреннийdependenciesЗначение актуально, и следующий фрагмент кода:

return useCallback(() => {
  const fn = ref.current;
  return fn();
}, [ref]);

Он выполняется только один раз (ссылка ref не изменится), поэтому он может возвращаться каждый разdependenciesявляется последнимfn,а такжеfnповторно выполняться не будет.

Предположим, у нас естьuseEventCallbackПереданная функция обратного вызова называется X,Смысл этого кода в том, что при закрытии каждого рендеринга callback-функция X всегда получает ту, что в последнем замыкании Renderer, поэтому зависимое значение всегда самое последнее, и функция не будет переинициализирована.

React Эта парадигма официально объявлена ​​устаревшей, поэтому для этого сценария используйтеuseReducer, передать функцию черезdispatchназывается в.ты помнишь?dispatchЭто своего рода черная магия, позволяющая обойти зависимости, о которых мы упоминали в разделе «Что такое useReducer».

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

PureRender с памяткой

В компоненте функции компонент классаPureComponentЭквивалентная концепцияReact.memo, давайте познакомимсяmemoПрименение:

const Child = memo((props) => {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return (
    // ...
  )
})

использоватьmemoОбернутый компонент будет перерисовываться для каждогоpropsЭлементы сравниваются неглубоко, и повторный рендеринг не запускается, если ссылка не изменилась. такmemoОтличный инструмент для оптимизации производительности.

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

Локальный PureRender с useMemo

в сравнении сReact.memoэтот инопланетянин,React.useMemoА вот серьезный официальный Хук:

const Child = (props) => {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return useMemo(() => (
    // ...
  ), [props.fetchData])
}

Как видите, мы используемuseMemoОберните код рендеринга так, чтобы даже если функцияChildпотому чтоpropsИзменения вносятся повторно, пока функция рендеринга используетprops.fetchDataБез изменений, без повторного рендеринга.

нашел здесьuseMemoПервое преимущество:Более детальный оптимизированный рендеринг.

Так называемый мелкозернистый оптимизированный рендеринг относится к функцииChildв целом можно использоватьA,Bдваprops, при рендеринге используется толькоB, затем используйтеmemoстроить планы,AИзменения могут привести к рендерингу и использованиюuseMemoплана не будет.

а такжеuseMemoПреимущества на этом не заканчиваются, давайте оставим здесь предзнаменование. Давайте сначала рассмотрим новую проблему: когда параметров становится все больше и больше, используйтеpropsПередача функций и значений между компонентами очень многословна:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const fetchData = useFetch(count, step);

  return <Child fetchData={fetchData} setCount={setCount} setStep={setStep} />;
}

несмотря на то чтоChildв состоянии пройтиmemoилиuseMemoДля оптимизации, ** но когда программа сложна, может быть несколько функций, общих для всех функциональных компонентов **, тогда необходим новый хук:useContextприйти на помощь.

Пакетная прозрачная передача с использованием Context

В функциональном компоненте вы можете использоватьReact.createContextСоздайте контекст:

const Store = createContext(null);

вnullначальное значение, обычно устанавливаемое наnullне важно. Далее есть два шага, которые нужно использовать на корневом узле.Store.ProviderВнедрить и использовать официальный хук на дочерних узлахuseContextПолучить введенные данные:

использовать в корневом узлеStore.Providerвпрыск:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const fetchData = useFetch(count, step);

  return (
    <Store.Provider value={{ setCount, setStep, fetchData }}>
      <Child />
    </Store.Provider>
  );
}

использовать на дочерних узлахuseContextПолучить введенные данные (т. е. получитьStore.Providerизvalue):

const Child = memo((props) => {
  const { setCount } = useContext(Store)

  function onClick() {
    setCount(count => count + 1)
  }

  return (
    // ...
  )
})

Таким образом, нет необходимости прозрачно передавать параметры между каждой функцией, а общедоступные функции можно разместить в контексте.

Но когда функций больше,Providerизvalueстанет очень раздутым, мы можем объединить ранее упомянутыеuseReducerрешить эту проблему.

использоватьuseReducerТоньше для передачи контента в контексте

использоватьuseReducer, все функции обратного вызова вызываютсяdispatchготово, затем просто передайте ContextdispatchФункция подойдет:

const Store = createContext(null);

function Parent() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });

  return (
    <Store.Provider value={dispatch}>
      <Child />
    </Store.Provider>
  );
}

Здесь, является ли это корневым узломProvider, или вызовы дочерних элементов очень освежают:

const Child = useMemo((props) => {
  const dispatch = useContext(Store)

  function onClick() {
    dispatch({
      type: 'countInc'
    })
  }

  return (
    // ...
  )
})

Вы можете вскоре понять, чтоstateтакже прошлоProviderНе лучше ли сделать инъекцию? Да, но здесь важно знать о возможных проблемах с производительностью.

БудуstateТакже поместите в контекст

слегка модифицированный, т.stateОн также помещен в Контекст, что очень удобно для присвоения и получения значения!

const Store = createContext(null);

function Parent() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 0 });

  return (
    <Store.Provider value={{ state, dispatch }}>
      <Count />
      <Step />
    </Store.Provider>
  );
}

правильноCount StepДля этих двух подэлементов нам нужно быть осторожными.Если мы реализуем эти два подэлемента следующим образом:

const Count = memo(() => {
  const { state, dispatch } = useContext(Store);
  return (
    <button onClick={() => dispatch("incCount")}>incCount {state.count}</button>
  );
});

const Step = memo(() => {
  const { state, dispatch } = useContext(Store);
  return (
    <button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
  );
});

результат:независимо от того, нажмитеincCountещеincStep, запустит средства визуализации обоих компонентов одновременно.

Проблема в:memoМы можем стоять только в самом внешнем слое, иuseContextВставка данных происходит внутри функции и будетобходитьmemo.

при срабатыванииdispatchПривести кstateизменяется, когда все используетсяstateВсе компоненты будут вынуждены обновляться. В это время, если вы хотите оптимизировать количество визуализации, вы можете вынуть толькоuseMemo!

useMemoСотрудничатьuseContext

использоватьuseContextкомпонент, если он не используется сам по себеprops, вы можете полностью использоватьuseMemoзаменятьmemoПроведите оптимизацию производительности:

const Count = () => {
  const { state, dispatch } = useContext(Store);
  return useMemo(
    () => (
      <button onClick={() => dispatch("incCount")}>
        incCount {state.count}
      </button>
    ),
    [state.count, dispatch]
  );
};

const Step = () => {
  const { state, dispatch } = useContext(Store);
  return useMemo(
    () => (
      <button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
    ),
    [state.step, dispatch]
  );
};

В этом примере, нажав соответствующую кнопку,Только используемые компоненты будут перерендерены, и эффект будет таким, как ожидалось.комбинироватьeslint-plugin-react-hooksИспользование плагина, дажеuseMemoВторой параметр зависимостей — все автодополнение.

Читая здесь, я не знаю, думаешь ли ты об этом.ReduxизConnect?

Давайте сравнимConnectа такжеuseMemo, найдет поразительное сходство.

Обычный компонент Redux:

const mapStateToProps = state => (count: state.count);

const mapDispatchToProps = dispatch => dispatch;

@Connect(mapStateToProps, mapDispatchToProps)
class Count extends React.PureComponent {
  render() {
    return (
      <button onClick={() => this.props.dispatch("incCount")}>
        incCount {this.props.count}
      </button>
    );
  }
}

Обычный функциональный компонент:

const Count = () => {
  const { state, dispatch } = useContext(Store);
  return useMemo(
    () => (
      <button onClick={() => dispatch("incCount")}>
        incCount {state.count}
      </button>
    ),
    [state.count, dispatch]
  );
};

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

Одной из причин появления хуков является облегчение статического анализа зависимостей и упрощение стоимости использования неизменяемых потоков данных.

мы видимConnectСценарий:

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

а такжеuseContext + useMemoСценарий:

из-за впрыскаstateОн заполнен. Вы можете использовать все, что хотите в функции рендеринга. Когда вы нажимаете клавишу сохранения,eslint-plugin-react-hooksпройдет статический анализ, вuseMemoВторой параметр автоматически заполняет внешние переменные, используемые в коде, такие какstate.count,dispatch.

Кроме того, можно обнаружить, что Context очень похож на Redux, поэтому как использовать асинхронную выборку, реализованную асинхронным промежуточным программным обеспечением в режиме компонента класса.useReducerсделай это? Ответ таков: это невозможно сделать.

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

Обработка побочных эффектов с помощью пользовательских хуков

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

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" });

      try {
        const result = await axios(url);
        if (!didCancel) {
          dispatch({ type: "FETCH_SUCCESS", payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: "FETCH_FAILURE" });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  const doFetch = url => setUrl(url);

  return { ...state, doFetch };
};

Как видите, пользовательский хук имеет полный жизненный цикл, мы можем инкапсулировать процесс выборки и выставлять только состояние — загружается ли он:isLoadingНе удалось получить:isErrorданные:data.

Очень удобно использовать в компонентах:

function App() {
  const { data, isLoading, isError } = useDataApi("https://v", {
    showLog: true
  });
}

Если это значение необходимо сохранить в потоке данных, совместно используемом всеми компонентами, мы можем объединитьuseEffectа такжеuseReducer:

function App(props) {
  const { dispatch } = useContext(Store);

  const { data, isLoading, isError } = useDataApi("https://v", {
    showLog: true
  });

  useEffect(() => {
    dispatch({
      type: "updateLoading",
      data,
      isLoading,
      isError
    });
  }, [dispatch, data, isLoading, isError]);
}

На этом вводная концепция функционального компонента завершена, и, наконец, добавлено пасхальное яйцо: как работать с DefaultProps функционального компонента?

Как установить defaultprops для функционального компонента?

Этот вопрос может показаться простым, но это не так. У нас есть как минимум два способа присвоить значения DefaultProps функционального компонента, которые объясняются ниже.

Прежде всего, для компонента класса существует только один способ написания DefaultProps, с которым согласны все:

class Button extends React.PureComponent {
  defaultProps = { type: "primary", onChange: () => {} };
}

Однако в функциональном компоненте это все виды вещей.

Используйте функции ES6 для назначения значений на этапе определения параметра

function Button({ type = "primary", onChange = () => {} }) {}

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

Посмотрите на эту сцену:

const Child = memo(({ type = { a: 1 } }) => {
  useEffect(() => {
    console.log("type", type);
  }, [type]);

  return <div>Child</div>;
});

если толькоtypeСсылка неизменна,useEffectне будет выполняться часто. Обновление через родительский элемент теперь приводит кChildПосле обновления мы обнаружили, чтоКаждый рендер будет распечатывать журнал, а это значит, что каждый раз, когда вы рендерите,typeЦитаты разные.

Есть менее элегантный способ решить эту проблему:

const defaultType = { a: 1 };

const Child = ({ type = defaultType }) => {
  useEffect(() => {
    console.log("type", type);
  }, [type]);

  return <div>Child</div>;
};

На этом этапе, если родительский элемент постоянно обновляется, журнал будет распечатан только один раз, потому чтоtypeСсылки те же.

Мы используем DefaultProps. Первоначальное намерение должно состоять в том, чтобы надеяться, что ссылка значения по умолчанию такая же,Если вы не хотите поддерживать ссылку на переменную отдельно, вы также можете позаимствовать встроенный в ReactdefaultPropsметод решения.

Используйте встроенные решения React

Встроенное решение React может лучше решить проблему частых изменений ссылок:

const Child = ({ type }) => {
  useEffect(() => {
    console.log("type", type);
  }, [type]);

  return <div>Child</div>;
};

Child.defaultProps = {
  type: { a: 1 }
};

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

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

Наконец, добавьте классический случай родительского компонента «яма» дочернего компонента.

Не связывайтесь с подкомпонентами

Мы делаем кнопку, которая нажимается и накапливается как родительский компонент, тогда родительский компонент будет обновляться после каждого нажатия:

function App() {
  const [count, forceUpdate] = useState(0);

  const schema = { b: 1 };

  return (
    <div>
      <Child schema={schema} />
      <div onClick={() => forceUpdate(count + 1)}>Count {count}</div>
    </div>
  );
}

Кроме того, мы будемschema = { b: 1 }Передайте его к детскому компоненту, это большая дыра, похороненная.

Код дочернего компонента выглядит следующим образом:

const Child = memo(props => {
  useEffect(() => {
    console.log("schema", props.schema);
  }, [props.schema]);

  return <div>Child</div>;
});

пока родительprops.schemaИзменения распечатают журнал. В результате, естественно, каждый раз, когда родительский компонент обновляется, дочерний компонент будет печатать журнал, т.е.Подсборка[props.schema]Полностью аннулирован, потому что ссылки продолжают меняться.

фактическиПодкомпоненты заботятся о значениях, а не о ссылках, поэтому одним из решений является переопределение зависимостей подкомпонента:

const Child = memo(props => {
  useEffect(() => {
    console.log("schema", props.schema);
  }, [JSON.stringify(props.schema)]);

  return <div>Child</div>;
});

Это гарантирует, что дочерние компоненты визуализируются только один раз.

Но настоящим виновником является родительский компонент, нам нужно использовать Ref для оптимизации родительского компонента:

function App() {
  const [count, forceUpdate] = useState(0);
  const schema = useRef({ b: 1 });

  return (
    <div>
      <Child schema={schema.current} />
      <div onClick={() => forceUpdate(count + 1)}>Count {count}</div>
    </div>
  );
}

такschemaСсылка всегда может оставаться неизменной. Если вы прочитаете эту статью полностью, вы сможете полностью понять первый пример.schema— это новая ссылка в каждом моментальном снимке рендеринга, а в случае RefschemaВ каждом моментальном снимке рендеринга есть только одна уникальная ссылка.

3. Резюме

Итак, вы начинаете работать с функциональным компонентом?

В этом интенсивном чтении остался вопрос: в каких деталях легко допустить ошибку в процессе разработки функционального компонента?

Адрес обсуждения:Интенсивное чтение «Введение в функциональный компонент» · Выпуск №157 · dt-fe/weekly

Если вы хотите принять участие в обсуждении, пожалуйста,кликните сюда, с новыми темами каждую неделю, выходящими по выходным или понедельникам. Интерфейс интенсивного чтения — поможет вам отфильтровать надежный контент.

Сфокусируйся наАккаунт WeChat для интенсивного чтения в интерфейсе

special Sponsors

Заявление об авторских правах: Бесплатная перепечатка - некоммерческая - не производная - сохранить авторство (Лицензия Creative Commons 3.0)