[Перевод] Глубокое погружение в принципы системы React Hook

внешний интерфейс Программа перевода самородков React.js внешний фреймворк

Как работает система React Hook

Мы вместе посмотрим, как это реализовано, и изучим React Hook изнутри.

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

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

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

Схематическая диаграмма системы хуков React


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

Dispatcher

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

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

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

Диспетчер вызывается функцией каждый раз, когда вызывается хукresolveDispatcher()Разбор.正如我之前所说,在 React 的渲染周期之外,这些都无意义了,React 将会打印出警告信息:"хуки можно вызывать только внутри функциональных компонентов"(видетьисходный код).

let currentDispatcher
const dispatcherWithoutHooks = { /* ... */ }
const dispatcherWithHooks = { /* ... */ }

function resolveDispatcher() {
  if (currentDispatcher) return currentDispatcher
  throw Error("Hooks can't be called")
}

function useXXX(...args) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useXXX(...args)
}

function renderRoot() {
  currentDispatcher = enableHooks ? dispatcherWithHooks : dispatcherWithoutHooks
  performWork()
  currentDispatcher = null
}

Обзор реализации диспетчера.


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

очередь крюка

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

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

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

{
  foo: 'foo',
  bar: 'bar',
  baz: 'baz',
}

Понимание состояния React со старой точки зрения

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

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}

Новые взгляды на состояние React

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

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

К сожалению, я еще не до конца разобрался с хуком редьюсера, потому что не могу воспроизвести ни один из его крайних случаев, поэтому трудно сказать об этой части. Все, что я могу сказать, это то, что реализация редьюсера довольно непоследовательна по сравнению с остальными, даже с его собственнойисходный кодАннотации в обоих состояниях «Не уверен, что это желаемая семантика»; так как я мог быть уверен? !

Итак, давайте вернемся к обсуждению хуков, прежде чем будет вызван каждый компонент функции, именованный[prepareHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123)будет вызываться первым, в котором текущее волокно и первый узел ловушки в очереди ловушек волокна будут сохранены в глобальной переменной. Таким образом, всякий раз, когда мы вызываем функцию ловушки (useXXX()), он знает текущий контекст.

let currentlyRenderingFiber
let workInProgressQueue
let currentHook

// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {
  currentlyRenderingFiber = workInProgressFiber
  currentHook = recentFiber.memoizedState
}

// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {
  currentlyRenderingFiber.memoizedState = workInProgressHook
  currentlyRenderingFiber = null
  workInProgressHook = null
  currentHook = null
}

// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:115
function resolveCurrentlyRenderingFiber() {
  if (currentlyRenderingFiber) return currentlyRenderingFiber
  throw Error("Hooks can't be called")
}
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:267
function createWorkInProgressHook() {
  workInProgressHook = currentHook ? cloneHook(currentHook) : createNewHook()
  currentHook = currentHook.next
  workInProgressHook
}

function useXXX() {
  const fiber = resolveCurrentlyRenderingFiber()
  const hook = createWorkInProgressHook()
  // ...
}

function updateFunctionComponent(recentFiber, workInProgressFiber, Component, props) {
  prepareHooks(recentFiber, workInProgressFiber)
  Component(props)
  finishHooks()
}

Обзор реализации очереди ловушек.

После завершения обновления файл с именем[finishHooks()](https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148)будет вызываться, в котором ссылка на первый узел в очереди ловушек будет сохранена в отрендеренном файбере.memoizedStateв свойствах. Это означает, что очередь ловушек и ее состояние могут быть расположены снаружи.

const ChildComponent = () => {
  useState('foo')
  useState('bar')
  useState('baz')

  return null
}

const ParentComponent = () => {
  const childFiberRef = useRef()

  useEffect(() => {
    let hookNode = childFiberRef.current.memoizedState

    assert(hookNode.memoizedState, 'foo')
    hookNode = hooksNode.next
    assert(hookNode.memoizedState, 'bar')
    hookNode = hooksNode.next
    assert(hookNode.memoizedState, 'baz')
  })

  return (
    <ChildComponent ref={childFiberRef} />
  )
}

Чтение состояния памяти компонента извне


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

State hook

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

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

Редуктор хука состояния, также известный как редуктор базового состояния.

Как вы понимаете, мы можем передать новое состояние прямо в диспетчер действий, но видите ли вы это? ! Мы также можем пройти вФункция действияк диспетчеру,Эта функция действия может получать старое состояние и возвращать новое.. (На момент написания этой статьи этот подход не был задокументирован вРеагировать на официальную документацию, к сожалению, это на самом деле очень полезно! ), что означает, что когда вы отправляете установщик состояния в дерево компонентов, вы можете изменить состояние родительского компонента, не передавая его в качестве другого свойства, например:

const ParentComponent = () => {
  const [name, setName] = useState()
  
  return (
    <ChildComponent toUpperCase={setName} />
  )
}

const ChildComponent = (props) => {
  useEffect(() => {
    props.toUpperCase((state) => state.toUpperCase())
  }, [true])
  
  return null
}

Возвращает новое состояние на основе старого состояния.


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

effect hook

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

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

Обратите внимание, что я использовал «рисовать» вместо «рендеринга». Они разные, в последнее времяРеагировать на конференцию, я видел, как многие ораторы используют эти два слова неправильно! даже в официальномРеагировать на документациюВ , там тоже написано "после рендеринга вступает в силу на экране", на самом деле этот процесс больше похож на "рисование". Функция рендеринга просто создает узел волокна, но ничего не рисует.

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

  • называется перед модификациейgetSnapshotBeforeUpdate()Пример (смисходный код).

  • Запустите выгрузку всех вставок, обновлений, удалений и ссылок (см.исходный код).

  • Запустите все функции жизненного цикла и функции обратного вызова ref. Функции жизненного цикла будут выполняться в отдельном канале, поэтому будут вызываться все замены, обновления и удаления во всем дереве компонентов. Этот процесс также запускает любые начальные хуки эффектов, специфичные для средства визуализации (см.исходный код).

  • useEffect()Эффекты, отправляемые хуком, также известные как «пассивные эффекты», основаны наэта часть кода(Может быть, мы начнем использовать этот термин в сообществе React?!).

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

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

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

Значение атрибута тега состоит из двоичных значений (см.исходный код):

const NoEffect = /*             */ 0b00000000;
const UnmountSnapshot = /*      */ 0b00000010;
const UnmountMutation = /*      */ 0b00000100;
const MountMutation = /*        */ 0b00001000;
const UnmountLayout = /*        */ 0b00010000;
const MountLayout = /*          */ 0b00100000;
const MountPassive = /*         */ 0b01000000;
const UnmountPassive = /*       */ 0b10000000;

Типы хуков, поддерживаемые React

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

const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)

Пример использования бинарного шаблона проектирования React

Вот эффекты ловушек, поддерживаемые React, и свойства их тегов (см.исходный код):

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

а вот как React проверяет триггеры поведения (см.исходный код):

if ((effect.tag & unmountTag) !== NoHookEffect) {
  // Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {
  // Mount
}

Выдержка из исходного кода

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

function injectEffect(fiber) {
  const lastEffect = fiber.updateQueue.lastEffect

  const destroyEffect = () => {
    console.log('on destroy')
  }

  const createEffect = () => {
    console.log('on create')

    return destroy
  }

  const injectedEffect = {
    tag: 0b11000000,
    next: lastEffect.next,
    create: createEffect,
    destroy: destroyEffect,
    inputs: [createEffect],
  }

  lastEffect.next = injectedEffect
}

const ParentComponent = (
  <ChildComponent ref={injectEffect} />
)

Пример вставки эффекта


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

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из ИнтернетаНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.