Глубокое погружение во внутреннюю работу, казалось бы, невозможного крючка

При обновлении контекста реакции все компоненты, использующие этот контекст, также обновляются. Это вызвало бы огромные проблемы с производительностью, если бы все компоненты с useSelector response-redux повторно отображались каждый раз, когда какая-либо часть хранилища redux изменялась. Так как же useSelector работает?

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

Хук useSelector от react-redux не имеет этой проблемы - компоненты повторно визуализируются только при изменении их выбранного фрагмента состояния, даже когда обновляются другие фрагменты хранилища. Итак, как это работает? Это как-то обходит правила контекста?

Конечно, ответ - «нет», но useSelector использует некоторые творческие тактики, чтобы добиться этого. Прежде чем мы начнем, давайте подробнее рассмотрим проблему. Надуманный пример контекста с двумя частями состояния, clicks и time, может выглядеть так:

Одна часть состояния (time) отслеживает количество секунд, прошедших с момента открытия приложения, другая (clicks) отслеживает, сколько раз пользователь нажимает кнопку. Вот как мы можем использовать их в нашем приложении:

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

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

Теперь, когда мы понимаем проблему, давайте посмотрим на useSelector. Если бы я построил react-redux с нуля, это могло бы выглядеть примерно так.

Хотя мой useSelector правильно возвращает состояние хранилища, он страдает той же проблемой, что и выше, и повторно отображает <Clicker /> каждый раз, когда обновляется таймер. Так как же useSelector от react-redux обойти это?

Я не автор и не эксперт по response / redux, но, покопавшись в исходном коде, я думаю, что у меня есть ответ: значение контекста response-redux на самом деле никогда не меняется.

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

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

Так как же компонент «узнает, когда нужно выполнить повторный рендеринг», если он не может полагаться на изменения своих свойств / контекста? Используется другой шаблон: подписка.

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

subscription.onStateChange = checkForUpdates

Посмотрите, где это происходит, в коде react-redux на GitHub

Подписка вызывает checkForUpdates, который проверяет, привело ли обновление магазина к изменению выбранного состояния. Если состояние изменилось, повторная визуализация запускается вызовом forceRender({}):

Посмотрите, где это происходит, в коде react-redux на GitHub

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

const [, forceRender] = useReducer(s => s + 1, 0)

Посмотрите, где это происходит, в коде react-redux на GitHub

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

selectedState = selector(store.getState())
...
return selectedState

Посмотрите, где это происходит, в коде react-redux на GitHub

Подводя итог, можно сказать, что подписка срабатывает при изменении любого состояния redux. Когда подписка срабатывает, она вызывает функцию в каждом экземпляре useSelector, которая проверяет, совпадает ли ее внутренняя ссылка на выбранное (старое) состояние хранилища с выбранным с использованием текущего (нового) состояния хранилища. Если они различаются, принудительно выполняется новый рендеринг, возвращающий обновленное состояние. Мне это не очень "реагирует" (что бы это ни значило), но это помогает! *

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

useSelector(state => {
  // Each time this runs it returns a brand new object
  return {
    thingOne: state.thingOne
    thingTwo: state.thingTwo
  }
})

Это можно смягчить, используя функцию равенства, например shallowEqual, в качестве второго аргумента для useSelector. См. Https://react-redux.js.org/next/api/hooks#equality-comparisons-and-updates

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

Https://codesandbox.io/s/peaceful-river-dj5ps
ПРИМЕЧАНИЕ. Хотя создание собственного useSelector - интересное упражнение, я НЕ рекомендую делать это в рабочем приложении!

Пожалуйста, дайте мне знать, было ли это полезно для вас или я что-то пропустил!

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

** Я использую шаблон контекста Кента С. Доддса, и мне он очень нравится - он не помогает с проблемой, обсужденной выше, но делает работу с контекстом намного удобнее: https://kentcdodds.com/blog / как использовать-реагировать-эффективно