Лучшие практики React Hooks [обновление]

React.js

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

Идея хуков React

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

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

Основной принцип

1. Попробуйте создать простые крючки

Первоначальная цель дизайна хуков состоит в том, чтобы сделать разработку быстрее и проще, поэтому при использовании хуков мы не должны скупиться на использование большего количества хуков, например, когда мы имеем дело с разными состояниями, соответствующими разной логике, согласно логике написания классов. , мы часто пишем несколько логик в функции жизненного цикла и различаем их с помощью if; при написании хуков, потому что нетshouldComponentUpdateДля таких функций жизненного цикла мы должны разделить их и написать в разныхuseEffectили использовать другойuseCallbackУпакованные зависимые переменные также должны быть максимально связаны с логикой, чтобы максимально избежать потери производительности и вывода ошибок.

2. Обратите внимание на логику внутри хуков В основном два принципа, упомянутые на официальном сайтереагировать JS.org/docs/hooks-…, очень важной концепцией, связанной с хуками, является порядок.Каждый раз, когда мы определяем функцию хука, реакция будет хранить их в «стеке» по порядку, подобноpic1Если в это время мы что-то сделаем и поместим одну из функций-ловушек в оператор if, например, установим firstName только в первом рендеринге, то это вызовет такую ​​ситуацию: первый рендеринг нормальный, но в При рендеринге во второй раз выполняется первая функция ловушки.

const [lastName, setLastName] = useState('yeung');

В это время react будет выполнять метод в стеке верхнего уровня, то есть наши последующие операции сдвигаются вперед на единицу.pic1

инициализация

Обычно мы используемuseStateчтобы создать переменную с состоянием, эта функция ловушки возвращает переменную состояния иsetter, когда мы звонимsetterфункция,renderФункция будет выполняться повторно, здесь общая проблема, использование несколькихstateили объединены в одинstate?

Проблема возникает из-за написанияuseSetStateдумая когда делаешь, как писалось ранееclassопыта, очевидно, что удобнее и управляемее писать все состояния вместе, но очевидно, что хуки неclass, собственно, здесьsetterМеханизм действия такжеsetStateРазные,setStateзаключается в объединении обновленных полей вthis.stateв и в крючкахsetterЭто прямая замена, поэтому, если мы поместим все переменные состояния в однуstate, очевидно, вопреки первоначальному замыслу более удобного обслуживания.

Но это не такstateЧем тоньше разделение, тем лучше.Взгляните на этот пример:

const [left, setLeft] = useState(0);

const [right, setRight] = useState(0);

el.addEventListener('mousemove', e => {

 setLeft(e.offsetX);

 setRight(e.offsetY);

})

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

const [position, setPosition] = useState({
 left: 0,
 right: 0,
})

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

операция по очистке

Задействованная здесь функция ловушкиuseEffect, согласно введению официальной документации, useEffect можно рассматривать какcomponentDidMount, componentDidUpdate, and componentWillUnmountКоллекция , DidMount и DidUpdate очень часто используются, здесь в основном говорят о том, какcomponentWillUnmountПрименение. существуетuseClickOut, мы добавили событие для документа.Очевидно, нам нужно выгрузить компонент, когда компонент выгружается.Метод здесь заключается в том, чтобыuseEffectФункция выгрузки выполняется при возврате , Для использования этой части на официальном сайте есть полное введение: React будет выполняться при размонтировании компонента и до повторного выполнения обратного вызова при изменении состояния зависимости.useEffectФункция, возвращаемая обратным вызовом, почему? Поскольку эффекты будут выполняться более одного раза за повторный рендеринг, предыдущие эффекты, конечно же, также будут очищены. Здесь следует отметить, что и операция удаления, и операция обратного вызова выполняются после возврата компонента.

Уменьшить дублирование рендеринга

React.memo

Функция этого метода аналогична функции в классеshouldComponentUpdate, разницаshouldComponentUpdateТо же будет сравнивать разницу в состоянии, ноReact.memoОн только сравнивает пропы, и правила сравнения тоже очень просты, он сравнивает пропсы до и после, чтобы решить, стоит ли перерисовывать, но на самом деле есть большие скрытые опасности, и некоторые блоггеры не рекомендуют его использовать.React.memo, но я думаю, что при соблюдении нескольких принципов,React.memoЭто действительно может в значительной степени сэкономить время рендеринга, особенно сейчас, когда оно используется.redux, часто необходимо избегать обновлений других состояний, вызывающих обновление текущего компонента. Во время оптимизации производительности необходимо детально рассчитать условия обновления компонентов.Как правило, добавляемые условия включают базовые типы, типы объектов должным образом сравниваются по глубине, а типы функций могут быть изменены в зависимости от ситуации, чтобы вся функция состоит всего из нескольких параметров. , если вы не можете быть уверены, то лучше использоватьPureComponentилиReact.memo.

useMemo

useMemo обычно используется для записи некоторых значений.Для начала давайте разберемся со сценариями использования useMemo:

1. Храните некоторые дорогостоящие переменные, чтобы избежать пересчета каждый раз при рендеринге;

2. Специально запишите некоторые значения, которые вы не хотите менять;

Что касается 2, вы можете использовать его напрямую.Что касается 1, мы должны решить, использовать ли его в зависимости от ситуации.Взгляните на следующую сцену:

const value = useMemo(() => {

 return massiveCompute(deps);

}, [deps]);

использовать или нетuseMemoв зависимости от:

1, MassComoute действительно большая операция, чтобы повлиять на производительность;

2. Тип данных deps, если это объект или массив, использовать useMemo бессмысленно, да и добавление сравнения скажется на производительности;

Сравнение UseEffect и ComponentDidMount

В официальной документации упоминается, что useEffect может реализовывать моки различных жизненных циклов, но на самом деле хуки механически отличаются от различных функций жизненного цикла.Если вообще отождествлять с жизненным циклом, то в последующих могут быть отклонения понимание. Конкретные различия см.useEffect is not the new ComponentDidMount, далее кратко поясняются проблемы, возникающие в процессе разработки.

Продолжительность

Первый дляcomponentDidMount, когда мы впервые вошли, если бы мы были вcomponentDidMountВыполнение государственной операции вcomponentDidMountдо и после, после чего браузер будет отображать только последний разrenderРендеринг, чтобы избежать заставки, т.е.componentDidMountна самом деле выполняется до того, как браузер отрисует; но дляuseEffect, хотя это также вызовет второй рендеринг, но второй рендеринг выполняется снова после отрисовки браузером, и этот эффект также вызовет заставку. УведомлениеuseEffectвыполняется один раз после возврата каждого компонента.

Различия в захвате времени состояния

думатьcomponentDidMountсценарий примененияcomponentDidMountАсинхронная операция выполняется в асинхронной операции.После разрешения асинхронной операции, если мы напечатаем состояние в это время, какой результат мы получим? Конкретный код можно посмотретьlongResolve with ComponentDidMount.

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

Тот же пример, если вы используетеuseEffectзаменятьComponentDidMountКак это будет? См. longResolve с использованием useEffect.

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

Почему это происходит?

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

 useEffect(() => {

   longResolve().then(() => {

     console.log(count);

   });

 }, [count]);

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

setInterval

При написании useInterval мы столкнулись с такой проблемой, если обрабатывать как в классе, то что мы делаем, так это пишем логику interval прямо в useEffect:

useEffect(() => {

   const id = setInterval(() => {

     setCount(count + 1)

   }, 1000);

   return () => clearInterval(id)

 }, [])

Результатом этого является то, что count сначала изменяется от 0 -> 1, а затем остается прежним. Причина та же, что и выше. Решение состоит в том, чтобы добавить соответствующую зависимую переменную -> count к deps, что может привести к тому, что мы беспокойтесь о том, чтобы вызвать бесконечный цикл, потому что мы одновременно меняем зависимые переменные, но учитываяsetIntervalИзначально это операция бесконечного цикла, так что здесь нет проблем.В то же время мы должны понимать, что пока мы находимся вuseEffectЕсли в deps используется переменная, необходимо добавить ее в deps.Если код имеет бесконечный цикл, то мы должны рассмотреть, нет ли проблемы с нашей внутренней логикой. Стоит отметить, что есть еще один способ написания функции setter, нам не нужно добавлять переменные в deps.

useEffect(() => {

 const id = setInterval(() => {

   // When we pass a function, React calls that function with the current

   // state and whatever we return becomes the new state.

   setCount(count => count + 1)

 }, 1000)

 return () => clearInterval(id)

}, [])

Разбор принципа хуков React по умолчанию

useMemo

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

function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
  currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
  workInProgressHook = createWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  if (workInProgressHook !== null) {
    const prevState = workInProgressHook.memoizedState;
    if (prevState !== null) {
      if (nextDeps !== null) {
        const prevDeps = prevState[1];
        if (areHookInputsEqual(nextDeps, prevDeps)) {
          return prevState[0];
        }
      }
    }
  }

  if (__DEV__) {
    isInHookUserCodeInDev = true;
  }
  const nextValue = nextCreate();
  if (__DEV__) {
    isInHookUserCodeInDev = false;
  }
  workInProgressHook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

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

useReducer & useState

useReducerа такжеuseStateПо существу принцип, хотя мы обычно используемuseStateбольше, но по фактуuseStateдаuseReducerпакет; внизу справаuseReducerРазберем принцип реализации;useReducerМожно разделить на первоначальный рендеринг иre-renderВо-вторых, сначала посмотрите на первоначальный рендеринг:

if (__DEV__) {
  isInHookUserCodeInDev = true;
}
let initialState;
if (reducer === basicStateReducer) {
  // Special case for `useState`.
  initialState =
    typeof initialArg === 'function'
      ? ((initialArg: any): () => S)()
      : ((initialArg: any): S);
} else {
  initialState =
    init !== undefined ? init(initialArg) : ((initialArg: any): S);
}
if (__DEV__) {
  isInHookUserCodeInDev = false;
}
workInProgressHook.memoizedState = initialState;
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
  last: null,
  dispatch: null,
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
  null,
  currentlyRenderingComponent,
  queue,
): any));
return [workInProgressHook.memoizedState, dispatch];

Из наиболее знакомого возвращаемого значения мы все знаем, чтоuseState(useReducer)Возвращает массив, индексы 0 и 1 соответственноstateа такжеdispatch, сначала посмотрите на состояние, где состояние прямо равно тому, что мы передали при первом рендеринге.useReducerпараметров (useReducerможно пройти еще одинinitфункция для получения начального состояния в качестве параметра и возврата соответствующего состояния); здесь основное внимание уделяется обработке отправки, здесьdispatchActionметод, функция этого метода состоит в том, чтобы сохранить метод обновления на карте с очередью в качестве ключа.dispatchActionИсходный код и конкретная функция:

function dispatchAction<A>(
  componentIdentity: Object,
  queue: UpdateQueue<A>,
  action: A,
) {
  if (componentIdentity === currentlyRenderingComponent) {
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // 这里的处理是当我们连续调用 dispatch 的时候,我们将 update 追加到已有的队列后面,而不是另起一个
      // 队列,这里在下次执行的时候可以将同步执行的 dispatch 合并到一个队列中,到时候也可以统一更新
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
  }
}

dispatchActionэто обновление на этапе рендеринга, скрывающее его в лениво созданной очереди -> список обновлений (renderPhaseUpdates). После завершения этого рендера мы перезапускаем и применяем скрытое обновление к незавершенному рабочему хуку (work-in-process)начальство.dispatchActionПолучены три параметра, которыеcomponentIdentity,queue,actionЗдесь bind используется для привязки, поэтому параметр действия — это параметр, передаваемый при вызове диспетчеризации. Пока что однаждыuseStateИнициализация завершена, фактически мы можем обнаружить, что когда мы вызываем диспетчеризацию, конкретная операция на самом деле не является модификацией.state, но добавляет соответствующее действие (или измененное значение) в очередь, когда повторный рендеринг рассчитывается какuseState, затем выполнить соответствующее обновление из этой глобальной очереди; давайте рассмотрим ситуацию, когда рендеринг повторяется, и покажем, что при повторении рендерингаuseReducerЛогика в:

// This is a re-render. Apply the new render phase updates to the previous
// current hook.
const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
  // Render phase updates are stored in a map of queue -> linked list
  const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
  if (firstRenderPhaseUpdate !== undefined) {
    renderPhaseUpdates.delete(queue);
    let newState = workInProgressHook.memoizedState;
    let update = firstRenderPhaseUpdate;
    do {
      // Process this render phase update. We don't have to check the
      // priority because it will always be the same as the current
      // render's.
      const action = update.action;
      if (__DEV__) {
        isInHookUserCodeInDev = true;
      }
      newState = reducer(newState, action);
      if (__DEV__) {
        isInHookUserCodeInDev = false;
      }
      update = update.next;
    } while (update !== null);

    workInProgressHook.memoizedState = newState;

    return [newState, dispatch];
  }
}
return [workInProgressHook.memoizedState, dispatch];

Во-первых, еслиrenderPhaseUpdatesимеет значение null, что указывает на то, что это обновление не обновлялось ранееdispatchвызов, затем возврат непосредственно по исходному значению; еслиrenderPhaseUpdatesНе нуль, что указывает на то, что ранееdispatchcall, но это обновление глобальное, поэтому на самом деле хуки не знают, что запускает обновление.queueхранится доrenderPhaseUpdatesВозьмите соответствующий метод обновления, если вы его получите, значит, это обновление было вызвано ранееdispatch, в это время операция обновления являетсяdo-whileцикл, логика здесь соответствуетdispatchActionЛогика создания очереди - объединит несколько апдейтеров в одну очередь, поэтому здесь цикл do-while выполняет все апдейтеры одновременно, обратите внимание на комментарии и логику здесь, то есть если мы находимся в серии диспетчеров напрямую Для изменение значения состояния, модификация здесь фактически сохраняет только последнюю модификацию, но если функция обратного вызова передается, напримерsetState((state) => state + 1)Затем вы можете получить последнее значение состояния, потому чтоnewStateОн меняется каждый раз.