Medium flux

Подробно о Flux

Flux это архитектура приложения, которую Facebook использует для создания на стороне клиента веб-приложений. Она дополняет компонуемые компоненты представления(view) React, используя однонаправленный поток данных. Это больше паттерн, нежели формальный подход, и вы можете начать использовать Flux сразу, не используя много нового кода.

Flux приложения имеют три основные части: диспетчер, хранилища, и отображения (вид, компоненты React). Их не следует путать с Model-View-Controller. Контроллеры существуют в (Flux)потоке приложения, но они контроллер-вид - вид часто встречается в верхней части иерархии, где извлекает данные из хранилищь и передаёт эти данные вплоть до их детей. Кроме того, создатели дейстия - вспомогательные методы диспетчера - используются для поддержки семантики API, которое описывает все изменения, которые возможны в приложении. О них полезно думать, как о четвертой части Flux(потока) цикла обновления.

Flux избегает MVC в пользу однонаправленного потока данных. Когда пользователь взаимодействует с React представлением(видом), вид распространяет действие через центрального диспетчера, в различные хранилища, которые содержат данные приложения и бизнес-логику, которая обновляет все виды, которые затронуты. Это особенно хорошо работает с декларативным стилем программирования React, который позволяет хранилищу посылать обновления без указания, как перемещать представления(виды) между состояниями.

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

Управление инвертируется с помощью хранилищ: хранилища принимают обновления и согласовывают их по мере необходимости, а не в зависимости от чего-то внешнего, что обновляет их данные логическим путем. Ничто за пределами хранилища не имеет какого-либо представления о том, как оно управляет данными для своей области, помогая сохранить четкое разделение задач. Хранилица не имеют прямых методов сеттер, как setAsRead(), но вместо этого имеют только один способ получения новых данных в их автономный мир - обратные вызовы, которые они регистрируют с помощью диспетчера.

Структура и поток данных

Данные в потоке приложения текут в одном направлении:

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

Представления (виды) могут вызвать новое действие распространяющееся через систему в ответ на действия пользователя:

Все данные текут через диспетчер в качестве центрального концентратора(хаба). Действия предоставляются диспетчеру в метод "action creator", и чаще всего происходят из взаимодействия пользователя с видом. Затем диспетчер вызывает функции обратного вызова, которые были зарегистрированы в хранилищах, распределяя действия на все хранилища. В рамках своих обратных вызовов, хранилища реагируют на какие-то действия, связанные с состоянием, которое они поддерживают. Затем хранилища испускают событие изменения предупредающее контроллер-видов, что измение в слое данных произошло. Контроллер-видов слушает эти события и извлекает данные из хранилищ, в обработчике событий. Контроллер-видов вызывает их собственный setState() метод, создающий повторный рендеринг себя и всех своих потомков в дереве компонентов.

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

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

Давайте посмотрим на различные части потока поближе. Давайте начнем с диспетчера.

Один Диспетчер

Диспетчер центральный распределитель, который управляет всем потоком данных во Flux приложении. По существу, это реестр обратных вызовов в хранилищах и не имеет никакого своего реального интеллекта - это простой механизм для распространения действия на хранилища. Каждое хранилище регистрирует себя и обеспечивает обратный вызов. Когда action creator обеспечивает диспетчер новым действием, все хранилища в приложении получают действие с помощью обратного вызова в реестре.

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

Тот же диспетчер, что Facebook использует в продакшн, доступен через npm, Bower, или GitHub.

Хранилища

Хранилища содержат состояние и логику приложения. Их роль немного похожа на модель в традиционном MVC, но они управляют состоянием многих объектов - они не представляют собой запись данных как делают модели ORM. Они не такие же, как коллекции Backbone. Больше, чем просто управление коллекциями объектов в ORM-стиле, хранилища управляют состоянием приложения для конкретной области внутри приложения.

Например, Facebook, Lookback Video Editor использовал TimeStore, чтобы следить за временем воспроизведения и состоянием воспроизведения. С другой стороны, ImageStore из того же приложения следит за коллекциями изображений. TodoStore в нашем TodoMVC примере похоже управляет коллекцией задач. Хранилище имеет характеристики как коллекции моделей, так и синглтона модели логической области.

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

Виды и контроллер-видов

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

Когда он получает событие из хранилища, он сначала запрашивает новые данные, которые ему нужны с помощью публичных getter методов хранилища. Затем он вызывает свои собственные setState() или forceUpdate() методы, в результате чего его render() метод и render() метод всех его потомков запускаются.

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

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

Действия

Диспетчер предоставляет метод, который позволяет нам вызвать отправку в хранилища, и включает загрузку данных, который мы называем действие. Создатель действия может быть обернут в семантический вспомогательный метод, который посылает действие диспетчеру. Например, мы хотим изменить текст пункта в списке приложения Дел. Мы создаем действие с сигнатурой функции, как updateText(todoId, newText) в нашем TodoActions модуле. Этот метод может быть вызван из обработчиков событий нашего вида, поэтому мы можем вызвать его в ответ на действия пользователя. Этот метод создателя действия также добавляет type к действию, так что, когда действие интерпретируется в хранилище, он может реагировать соответствующим образом. В нашем примере, этот тип может быть назван чем-то вроде TODO_UPDATE_TEXT.

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

Как насчет диспетчера?

Как упоминалось ранее, диспетчер также может управлять зависимостями между хранилищами. Эта функциональность доступна через waitFor() метод в классе Dispatcher. Мы не должны использовать этот метод в чрезвычайно простом приложении TodoMVC, но это становится жизненно важным в более крупных и сложных приложениях.

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

case 'TODO_CREATE':
  Dispatcher.waitFor([
    PrependedTextStore.dispatchToken,
    YetAnotherStore.dispatchToken 
  ]);

  TodoStore.create(PrependedTextStore.getText() + ' ' + action.text);
  break;

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

register() возвращает диспетчерский маркер при регистрации обратных вызовов для диспетчера (Dispatcher):

PrependedTextStore.dispatchToken = Dispatcher.register(
  function (payload) { 
    // ... 
  });

Где Диспетчер подходит для потока данных

Диспетчер - синглтон, и работает как центральный концентратор потока данных в потоке приложения. По существу, это реестр обратных вызовов, и может вызывать эти обратные вызовы в заданном порядке. Каждое хранилище регистрирует обратный вызов в диспетчере. Когда новые данные поступают в диспетчер, то используются эти обратные вызовы, чтобы распространить эти данные на все хранилища. Процесс вызова функции обратного вызова инициируется посредством метода dispatch(), который принимает объект полезных данных (payload) в качестве единственного аргумента.

Действия и ActionCreators

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

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

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

Зачем нам нужен Диспетчер

По мере роста приложение, зависимости между различными хранилищами растут. Хранилище А неизбежно будет нужндаться в хранилище B, чтобы обновить себя, прежде чем, хранилище B узнает, как обновить себя. Нам нужно чтобы диспетчер имел возможность ссылаться на функцию обратного вызова для хранилища B, и завершил эту функцию обратного вызова, прежде чем двигаться дальше с хранилищем А. Чтобы декларативно утвердить эту зависимость, хранилище должно быть в состоянии сказать диспетчеру, "Мне нужно ждать чтобы хранилище B завершило обработку этого действия". Диспетчер предоставляет эту функциональность с помощью метода waitFor().

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

Кроме того, метод waitFor() может быть использован по-разному для различных действий, в рамках обратного вызова того же хранилища. В одном случае, хранилищу A, возможно, потребуется подождать Хранилище B. Но в другом случае, ему, возможно, потребуется подождать Хранилище С. Использование waitFor() в блоке кода, который является специфичным для действия позволяет нам иметь точный контроль из этими зависимостями.

Однако, если у нас есть циклические зависимости, то возникают проблемы. То есть, если хранилище A должно ждать хранилище B, и хранилище B должно ждать хранилище А, то мы могли бы скатиться в бесконечный цикл. Диспетчер в Flux способен защитить от этого, выбрасывая информативную ошибку, чтобы предупредить разработчиков, у которых произошла эта проблема. Разработчик может создать третье хранилище и решить циклическую зависимость.