Смотрим на реализацию и знакомимся с ней наизнанку

Мы все об этом слышали. Новая система хуков React 16.7 наделала много шума в сообществе. Мы все это пробовали и тестировали, и мы были очень взволнованы этим и его потенциалом. Когда вы думаете о хуках, они в некотором роде волшебны, React каким-то образом управляет вашим компонентом, даже не открывая его экземпляр (без использования ключевого слова this). Так как же, черт возьми, это делает React?

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

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

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

Диспетчер

Диспетчер - это общий объект, который содержит функции ловушки. Он будет динамически выделяться или очищаться в зависимости от фазы рендеринга ReactDOM, и это гарантирует, что пользователь не получит доступ к хукам за пределами компонента React (см. Реализация).

Перехватчики включаются / отключаются флагом enableHooks прямо перед рендерингом корневого компонента путем простого переключения на правый диспетчер; это означает, что технически мы можем включать / отключать перехватчики во время выполнения. В React 16.6.X также реализована экспериментальная функция, но она фактически отключена (см. Реализация).

По завершении рендеринга мы обнуляем диспетчер и тем самым предотвращаем случайное использование хуков вне цикла рендеринга ReactDOM. Это механизм, который гарантирует, что пользователь не будет делать глупостей (см. Реализация).

Диспетчер разрешается при каждом вызове ловушки с помощью функции, называемой resolveDispatcher(). Как я сказал ранее, вне цикла рендеринга React это не должно иметь смысла, и React должен вывести предупреждающее сообщение: Хуки могут быть вызваны только внутри тела функционального компонента (см. Реализация ).

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

Очередь крючков

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

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

Соответственно, нам нужно переосмыслить то, как мы рассматриваем состояние компонента. До сих пор мы думали об этом как о простом объекте:

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

Схему отдельного хука-узла можно посмотреть в разделе Реализация. Вы увидите, что у крючка есть некоторые дополнительные свойства, но ключ к пониманию того, как работают хуки, лежит в memoizedState и next. Остальные свойства используются ловушкой useReducer() для кэширования отправленных действий и базовых состояний, поэтому процесс сокращения может быть повторен в качестве запасного варианта в различных случаях:

  • baseState - объект состояния, который будет передан редуктору.
  • baseUpdate - последнее отправленное действие, создавшее baseState.
  • queue - очередь отправленных действий, ожидающих выполнения редуктором.

К сожалению, мне не удалось хорошо разобраться в крючке редуктора, потому что мне не удалось воспроизвести почти ни один из его крайних случаев, поэтому мне было бы неудобно вдаваться в подробности. Я только скажу, что реализация редуктора настолько непоследовательна, что даже в одном из комментариев в самой реализации говорится, что (он) не уверен, что это желаемая семантика; так как я должен быть уверен ?!

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

После завершения обновления будет вызвана функция с именем finishHooks(), где ссылка на первый узел в очереди перехватчиков будет сохранена на визуализированном волокне в свойстве memoizedState. Это означает, что очередь хуков и их состояние могут быть адресованы извне:

Давайте более конкретно и поговорим об отдельных хуках, начиная с самого распространенного - хука состояния:

Государственные крючки

Вы будете удивлены, узнав, но за кулисами хук useState использует useReducer и просто предоставляет ему предопределенный обработчик редуктора (см. Реализация). Это означает, что результаты, возвращаемые useState, на самом деле являются состоянием редуктора и диспетчером действий. Я хотел бы, чтобы вы взглянули на обработчик редуктора, который использует обработчик состояния:

Итак, как и ожидалось, мы можем напрямую предоставить диспетчеру действий новое состояние; но вы бы посмотрели на это ?! Мы также можем предоставить диспетчеру функцию действия, которая получит старое состояние и вернет новое. Этот не правда документирована где в ̶̶̶ Официальном Реагировать ДОКУМЕНТАЦИЮ ·· (Что же касается времени написания этой статьи) ̶̶̶ и ̶̶̶ ̶t̶h̶a̶t̶'̶s̶ жаль, потому что подмигнул чрезвычайно полезно! ·· Это означает, что при отправить установщик состояния вниз по дереву компонентов, вы можете запускать мутации в отношении текущего состояния родительского компонента, не передавая его как другую опору. Например:

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

Крючки для эффектов

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

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

Обратите внимание, что я использую термин рисование, а не рендеринг. Это разные вещи, и я видел, как многие ораторы на недавней конференции React Conf использовали неправильный термин! Даже в официальных документах React docs говорится после того, как рендеринг передан на экран, что похоже на рисование. Метод рендеринга просто создает узел волокна, но еще ничего не рисует.

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

  • Вызов экземпляров getSnapshotBeforeUpdate() перед мутацией (см. Реализация).
  • Выполните все операции по вставке, обновлению, удалению и отключению ссылок (см. Реализация).
  • Выполните все жизненные циклы и обратные вызовы ref. Жизненные циклы выполняются как отдельный проход, поэтому все размещения, обновления и удаления во всем дереве уже были вызваны. Этот проход также вызывает любые начальные эффекты, зависящие от рендерера (см. Реализация).
  • Эффекты, запланированные с помощью хука useEffect(), также известные как пассивные эффекты в зависимости от реализации (может быть, нам стоит начать использовать этот термин в сообществе React ?!).

Когда дело доходит до эффектов перехвата, они должны храниться на волокне в свойстве с именем updateQueue, и каждый узел эффекта должен иметь следующую схему (см. Реализация):

  • tag - двоичное число, которое будет определять поведение эффекта (скоро я уточню).
  • create - обратный вызов, который должен выполняться после рисования.
  • destroy - обратный вызов, возвращенный из create(), который должен выполняться до первоначального рендеринга.
  • inputs - набор значений, определяющих, следует ли уничтожить и воссоздать эффект.
  • next - ссылка на следующий эффект, который был определен в функции Component.

Помимо свойства tag, другие свойства довольно просты и понятны. Если вы хорошо изучили хуки, то знаете, что React предоставляет вам пару хуков со специальными эффектами: useMutationEffect() и useLayoutEffect(). Эти два эффекта внутренне используют useEffect(), что по сути означает, что они создают узел эффекта, но делают это с использованием другого значения тега.

Тег состоит из комбинации двоичных значений (см. Реализация):

Наиболее распространенные варианты использования этих двоичных значений - использование конвейера (|) и добавление битов как есть к одному значению. Затем мы можем проверить, реализует ли тег определенное поведение или нет, используя амперсанд (&). Если результат не равен нулю, это означает, что тег реализует указанное поведение.

Вот поддерживаемые React типы эффектов хуков и их теги (см. Реализация):

  • Эффект по умолчанию - UnmountPassive | MountPassive.
  • Эффект мутации - UnmountSnapshot | MountMutation.
  • Эффект макета - UnmountMutation | MountLayout.

А вот как React проверяет реализацию поведения (см. Реализация):

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

Вот и все! Что вы сделали из этой статьи больше всего? Как вы собираетесь использовать эти новые знания в своих приложениях React? Очень хотелось бы увидеть интересные комментарии!