Redux в картинках

Это перевод статьи про Redux от Лин Кларк: http://code-cartoons.com/a-cartoon-intro-to-redux-3afb775501a6

Перевод также доступен здесь: medium.com/russian/a-cartoon-intro-to-redux-e2108896f7e6

 

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

Если Вы не читали последнюю статью про Flux, сделайте это сейчас.

Зачем менять Flux?

Redux решает те же проблемы, что и Flux, плюс ещё несколько.

Как и Flux, он делает изменения состояния в приложениях более предсказуемым. Если Вы хотите изменить состояние (state), Вы должны вызвать действие (action). Нет другого способа изменить состояние напрямую, потому что та штука, которая содержит состояние (т.е. хранилище (store)), имеет только геттер, но ни одного сеттера. Эти основные положения Flux и Redux очень похожи.

Но зачем же ещё один шаблон? Создатель Redux, Данил Абрамов, увидел возможность улучшить Flux. Он хотел использовать улучшенные инструменты разработчика. И понял, что изменение всего пары “штук” сделает возможными улучшенные инструменты разработчика, но в то же время останется предсказуемость, которую даёт Flux.

Он мечтал о загрузке кода “на лету” и “путешествиях во времени” в ходе отладки (статья с объяснением). Но существовали некоторые проблемы, не дававшие инструментам разработчика легко взаимодействовать с Flux.

Проблема №1: Код хранилищ не может быть перезагружен без обнуления состояния

Во Flux, хранилище содержит 2 вещи:

  • Логику изменения состояния
  • Текущее состояние

Содержание двух этих сущностей в одном объекте создаёт проблему для перезагрузки “на лету”. При перезагрузке объекта хранилища (чтобы увидеть эффект, вызванный изменением логики нового состояния) Вы потеряете предыдущие состояния, которое содержало хранилище. К тому же, Вы потеряете подписки на события, связывающие хранилище и остальную систему.

Решение

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

Проблема №2: Состояние перезаписывается при каждом действии

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

Каждый раз, когда меняется состояние, Вам необходимо добавить старое состояние в массив предыдущих состояний. Но из-за способа, которым работает JavaScript, простое добавление переменной к массиву не сработает. “Снимок” объекта не будет создан, а только лишь появится новый указатель на тот же самый объект.

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

Решение

Когда действие приходит в хранилище, не управляйте им с помощью изменения состояния. Вместо этого скопируйте состояния и примените изменения к копии.

Проблема №3: Нет правильных “мест” для сторонних плагинов

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

Чтобы это сработало, Вам нужны “точки расширения”… то есть участки кода, в которые можно что-то дописать.

Например, логирование. Допустим, Вы хотите записывать каждое приходящее действие с помощью console.log(), а затем выводить также полученное в результате действия состояние. Во Flux, Вам надо было подписаться на обновления диспетчера и на обновления каждого хранилища. Но это же пользовательский код, а не то, что может быть легко получено с помощью стороннего модуля.

Решение

Сделать легким “оборачивание” частей системы в другие объекты. Эти другие объекты добавляют свою небольшую функциональность поверх оригинала. Вы можете видеть эти “точки расширения” в “усилителях” или объектах “высшего порядка”, а также в промежуточном ПО.

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

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

Давайте посмотрим, как Redux делает всё это возможным.

Новые персонажи

Набор персонажей немного изменился по сравнению с Flux.

Создатели действия (Action creators)

Redux оставил создателя действия таким же, каким он был у Flux. Когда бы Вы ни захотели изменить состояние приложения, Вы вызываете действие. Это единственный способ, с помощью которого должно изменяться состояние.

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

В отличие от Flux, создатели действия в Redux не оптравляют действие диспетчеру. Вместо этого она возвращают отформатированный объект действия.

 

 

 

 

Хранилище (The store)

В статье о Flux хранилища были описаны как вышестоящие бюрократы. Все изменения состояния должны быть сделаны лично ими и пройти через все надлежащие процедуры. Хранилище в Redux’е по-прежнему остаётся контролирующим и бюрократичным, но всё же немного отличается.

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

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

Вместо этого, оно заботится только о дереве состояния. Затем оно рапсределяет работу по выяснению, какое изменения состояния должно произойти. Редюсеры (reducers), возглавляемые корневым редюсером, возьмут на себя эту задачу.

Вы могли заметить, что здесь нет диспетчера. Всё из-за того, что при “захвате власти” хранилище забрало себе и диспетчеризацию.

 

Редюсеры (The reducers)

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

Я представляю редюсеры, как отдел “белых воротничков”, которые немного переусердствовали в ксерокопировании. Они не хотят что-то сломать, поэтому не изменяют полученное ими состояние. Вместо этого, они делают копию и проделывают все свои изменения на ней.

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

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

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

Представления: “умные” и “глупые” компоненты (The views: smart and dumb components)

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

В Redux есть аналогичная концепция: “умные” и “глупые” компоненты. “Умные” компоненты — это менеджеры. Они подчиняются ещё нескольким правилам, по сравнению с представлениями контроллеров, однако:

  • “Умные” компоненты в ответе за действия. Если подчинённому им “глупому” компоненту нужно вызвать действие, “умный” компонент передает ему функцию через свойства. “Глупый” компонент просто может использовать её в качестве обратного вызова.
  • У “умных” компонентов нет собственных CSS-стилей.
  • “Умные” компоненты редко имеют дело с DOM. Вместо этого, они договариваются с “глупыми” компонентами, которые отвечают за обработку элементов DOM.

“Глупые” компоненты не зависят непосредственно от действий, так как все действия передаются через свойства. Это означает, что “глупый” компонент может быть повторно использован в другом приложении с другой логикой. Они также содержат стили, с помощью которых выглядят достаточно хорошо (однако Вы можете добавить и пользовательские стили, просто примените свойство стиля и соедините его со стилями “по умолчанию”).

Связывание уровня представления (The view layer binding)

Redux нуждается в небольшой помощи при подключении хранилища к представлениям. Нужно что-то, что соединит их воедино. И это — связывание уровня представления. Если вы используете React, то это react-redux.

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

Связывание уровня представления внедряет три концепции:

  • Провайдер (provider): он оборачивает дерево компонентов. Также он позволяет потомкам главного компонента легко подлючиться к хранилищу, используя connect().
  • connect(): функция, предоставляемая в react-redux. Если компонент хочет получать обновления состояния, он оборачивает себя в connect(). Затем эта функция с помощью селектора (selector) будет настраивать всё за него.
  • Селектор (selector): это функция, которую пишите Вы. Она определяет, какие части состояния нужны компоненту в качестве свойств.

 

Корневой компонент (The root component)

Все приложения на React имеют корневые компоненты. Это всего лишь компонент на вершине иерархии других компонентов. Но в приложениях Redux этот компонент принимает на себя ещё больше ответственности.

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

Корневой компонент становится автоматическим после инициализации приложения. Он не увязнет в каждодневных заботах вызывания перерисовки. Он оставляет это на совести подчинённых компонентов с помощью связывания уровня представления.

 

 

 

Как они работают вместе

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

Настройка

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

1. Подготовить хранилище. Корневой компонент создаёт хранилище, говоря ему, какой корневой редюсер использовать, с помощью функции createStore(). Этот корневой редюсер уже имеет команду подчинённых редюсеров, которые будут ему всё сообщать. Команда редюсеров собирается с помощью функции combineReducers().

Корневой компонент: “Привет, хранилище. Ты в деле! Вот корневой редюсер. Он придёт вместе со своей командой. Только посмотрите, какая у меня команда, ответственная за состояние.”

2. Установить связь между хранилищем и компонентами. Корневой компонент оборачивает свои подкомпоненты с помощью провайдера и создает связь между хранилищем и провайдером.

Провайдер создаёт то, что обычно называют сетью, для обновления компонентов. “Умные” компоненты подключаются к сети с помощью connect(). Это гарантирует, что они получат обновления состояния.

Корневой компонент: “Провайдер, вот хранилище, которое я только что нанял. Оно будет давать тебе обновления. Не мог бы ты настроить что-то для получения этих обновлений компонентами?” Провайдер: “О, отлично! Дай-ка я подключусь, чтобы получать обновления”

3. Подготовить обратные вызовы для действий. Чтобы “глупым” компонентам было проще работать с действиями, “умные” компоненты могут настроить обратные вызовы для действия с помощью bindActionCreators(). Таким образом, они могут просто передать обратный вызов “глупому” компоненту. Действие будет автоматически отправлено после форматирования.

“Умный” компонент: “Мне нужно, чтобы “глупые” с этим легко справились… Лучше всего будет связать создателя действия и функцию отправления вместе, так что “глупый” просто использует функцию обратного вызова.”

Поток данных

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

Это число должны быть равным 10.

1. Представление запрашивает действие. Создатель действия форматирует и возвращает его.

“Глупый” компонент: “Мне нужна обновленная статистика действия. Установите это число равным 10.”

2. Действие либо будет отправлено автоматически (если при настройке была использована функция bindActionCreators()), либо будет отправлено с помощью представления.

3. Хранилище получает действие. Оно посылает текущее дерево состояния и само действие корневому редюсеру.

Хранилище: “А вот и текущее дерево состояния. Пусть твоя команда разберётся, как будет выглядеть новое дерево состояния.”

4. Корневой редюсер разделяет дерево состояния на части. Затем он передаёт каждый кусочек в подчинённый редюсер, который в курсе, что с ним делать.

Редюсер: “А вот и твоя часть”

5. Подчинённый редюсер копирует эту часть и вносит изменения в копию. Он возвращает копию корневому редюсеру.

Подчинённый редюсер: “Дай мне минутку, я скопирую свою часть… и сейчас немного изменю её… Всё, готово! Вот, как эта часть должна в итоге выглядеть.”

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

7. Хранилище говорит связыванию уровня представления, что появилось новое состояние.

Хранилище: “У нас для тебя новое состояние.”

8. Связывание уровня представления просит хранилище переслать новое состояние.

Связывание уровня представления: “А вот и новое дерево состояния. Пожалуйста, передай его мне.”

9. Связывание уровня представления вызывает перерисовку.

Связывание уровня представления: “Ау? Извините, спасибо Вам. Пожалуйста, обратите внимание на новое дерево состояния и перерисуйте соответствующее дерево компонентов.”

Вот, что я думаю о Redux и его отличиях от Flux. Надеюсь, эта статья оказалась полезной!

 

Последние твиты

Контакты