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

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

Но сначала, что такое Redux? Согласно документам Redux:

«Redux — это контейнер с предсказуемым состоянием для приложений JavaScript».

Другими словами, redux помогает вам управлять состоянием для веб-приложений JavaScript. Чаще всего он используется вместе с React, но может использоваться вместе с любым фреймворком, таким как Vue, Angular, или для ванильных приложений вообще без фреймворка.

Выше показаны строительные блоки архитектуры Redux; действия, редуктор, сохранение и просмотр. Наша пользовательская реализация Redux будет следовать приведенному выше шаблону.

Состояние приложения управляется в файле store. Само состояние — это обычный объект JavaScript. Хранилище также предоставляет методы для обновления состояния и чтения из состояния.

Redux использует шаблон публикации/подписки (PubSub) для обработки обновлений состояния. Когда пользователь взаимодействует с пользовательским интерфейсом, действия могут быть отправлены (опубликованы) для обновления состояния. Представления могут прослушивать (подписываться) на эти обновления и соответствующим образом изменять пользовательский интерфейс. Действия в Redux — это простые объекты JavaScript с атрибутом type, содержащим уникальный ключ для действия, и атрибутом payload, содержащим данные.

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

На этих концепциях основаны три основных принципа Redux:

Единый источник достоверной информации

Состояние всего вашего приложения хранится в дереве объектов в одном хранилище.

Состояние доступно только для чтения

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

Изменения вносятся с помощью чистых функций

Чтобы указать, как дерево состояний преобразуется действиями, мы пишем чистые редукторы (чистые = функции, которые возвращают заданный вывод для каждого заданного ввода).

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

Создание редукса

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

Если упростить вышесказанное, магазин выглядит примерно так:

const store = {
  state: {}, // a plain object holding our state
  listeners: [], // an array containing all listeners (subscribers)
  getState: () => {}, // function to get the current state
  dispatch: () => {}, // function to dispatch updates to the state
  subscribe: () => {}, // subscribe function to add new listeners
}

В полной реализации видно, что все происходит внутри функции create store. Эта функция принимает два аргумента; reducer и initialState. Как упоминалось ранее, редюсеры — единственные, кому разрешено изменять состояние. Они делают это, не изменяя состояние, а возвращая новое состояние с действием, примененным к предыдущему состоянию. Аргумент initialState просто позволяет нам создать новое хранилище с некоторыми предопределенными данными.

В нашей реализации state, listeners и getState говорят сами за себя. Состояние — это просто объект, содержащий наше состояние. Listeners — это массив, содержащий всех слушателей (подписчиков), а getState — это функция, возвращающая текущее состояние. Функция подписки добавляет новых подписчиков в массив слушателей. Подписчик — это функция, которая будет выполняться каждый раз при обновлении состояния.

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

Редуктор и действия

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

В этом примере у нас есть два типа действий для изменения состояния: ADD и SUBTRACT. Если мы отправим действие с типом «добавить», редюсер добавит число в полезной нагрузке к числу в текущем состоянии. Затем редьюсер возвращает обновленное состояние. Если мы отправим любое другое действие, кроме добавления или вычитания, редюсер вернется к значению по умолчанию и вернет текущее состояние без изменений.

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

store.listeners.forEach((listener) => listener());

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

Вид

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

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

Все вместе получаем что-то вроде этого:

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

Заворачивать

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