Программы — это преобразователи данных. Мы даём программе задачу: вводим начальные данные и ожидаем получить какой-то результат после их преобразования.
Сложные программы делят исходную задачу на задачи поменьше. За разные подзадачи отвечают разные части программы — функции, классы, модули.
Чтобы программа выполнила исходную задачу целиком, её части должны работать сообща. Они общаются друг с другом с помощью промежуточных результатов работы — тоже данных.
То, как одна часть программы получает и передаёт данные другим, называется потоком данных (data flow) и может определить архитектурное устройство всей системы.
Виды потоков данных
СкопированоВ целом, потоки данных могут быть организованы огромным количеством способов, но чаще всего на практике во фронтенде встречаются два:
- однонаправленный (one-way).
- двунаправленный (two-way).
Однонаправленный поток
СкопированоВ однонаправленном потоке данных каждая часть программы от другой части может либо получить данные, либо передать. Направление такого потока не меняется.
Однонаправленный поток можно схематично представить, как водопровод, а модуль — как часть трубы:
В таком потоке данные «текут» от одного модуля к другому, а выходные данные предыдущего становятся входными следующего:
Flux / Redux
СкопированоСамый известный пример архитектуры с однонаправленным потоком данных — это Flux и, как его реализация, Redux.
Во Flux приложение состоит из 3 главных компонентов:
- хранилище данных или стор, store;
- диспетчер, dispatcher;
- представление или вью, view.
Задача стора похожа на задачу модели из MVC — он хранит в себе данные. Изменение данных в сторе влечёт за собой изменение представления, то есть перерисовку пользовательского интерфейса.
Задача представления — показать данные в понятном для пользователя виде, нарисовать пользовательский интерфейс.
Когда пользователи совершают какие-то действия, например, нажимают на кнопки, представление вызывает экшен (action) — объект-команду, который говорит, что произошло.
Экшен попадает в диспетчер, он распространяет этот экшен всем модулями, которые знают, как обработать его. (В Redux такие модули называются редьюсеры, reducers.) Эти модули преобразовывают данные в хранилище. Обновление данных влечёт перерисовку представления, и цикл замыкается.
Такой поток данных похож на классический MVC.
Двунаправленный поток
СкопированоДанные в двунаправленном потоке могут передаваться между частями программы в обе стороны.
Чаще всего это используется для связывания модели и представления, чтобы обновление, например, текста в поле ввода сразу обновило данные в модели — это называется двунаправленным связыванием данных (two-way data binding).
У такого «среза углов» есть и плюсы, и минусы. Из плюсов:
- Меньше кода, потому что не надо писать экшен и обработчик для него.
- Работает как магия, если фреймворк делает всё автоматически за разработчиков.
Из минусов:
- Труднее отлаживать, когда двойное связывание используется для чего-то сложнее, чем обновление текста в поле ввода.
- Это работает как магия, если фреймворк делает всё автоматически :–)
Реактивность
СкопированоФреймворки, которые используют двунаправленное связывание, часто реактивные — то есть применяют изменения мгновенно не только к UI, но и к вычисляемым данным.
Представьте, что мы используем Excel. Запишем в двух ячейках численные значения, а в следующей — функцию подсчёта их суммы:
В третьей ячейке будет посчитанное значение:
Если мы теперь изменим значение первой ячейки, значение суммы пересчитается автоматически.
Это автоматическое изменение значения в третьей ячейке — и есть реактивность. Кажется, что это ничем не отличается от простого двойного связывания, но разница есть.
Представим, что мы пишем код, который должен делать то же самое:
function sum(a, b) { return a + b}let a1 = 2let a2 = 3let a3 = sum(a1, a2)// 5
function sum(a, b) { return a + b } let a1 = 2 let a2 = 3 let a3 = sum(a1, a2) // 5
Если мы изменим значение a1
, значение a3
не поменяется:
a1 = 22console.log(a3)// 5
a1 = 22 console.log(a3) // 5
Чтобы значение реактивно отреагировало (to re-act) на изменение, нам нужно перезапустить его подсчёт:
a3 = sum(a1, a2)// 25
a3 = sum(a1, a2) // 25
Фреймворки типа Vue берут на себя заботу о реактивности значений.
Что выбрать
СкопированоЗависит от задачи :–)
Нет строгих рекомендаций к выбору способа организации потоков данных. Можно учитывать, насколько будет удобно писать код и отлаживать его.
Например, в больших или сложных приложениях или больших командах хочется меньше «магии», чтобы чётко понимать, что и при каких условиях происходит — однонаправленный поток может это обеспечить.
В небольших приложениях двунаправленный поток может сэкономить много времени, потому что заменяет собой однотипный код, который бы пришлось писать в случае с однонаправленным потоком.