Основы программирования на примере исходного кода MobX

c08d28b9b03b5e4c095f0a36b8654674.png

Структуры данных

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

Атом

Представляет базовую единицу наблюдаемости. Класс Atom в atom.ts хранит текущее состояние наблюдаемого значения и управляет взаимодействиями наблюдателей и обновлениями состояния. Он предоставляет методы:
Отслеживание наблюдателей: reportObserved вызывается, когда производная обращается к значению атома, позволяя атому записывать производную в качестве наблюдателя.
Уведомление наблюдателей: reportChanged вызывается, когда значение атома изменяется, уведомляя всех наблюдателей о том, что они могут быть устаревшими и их необходимо перезапустить.

Производные (наблюдатели)

Абстрактное понятие, охватывающее как ComputedValue, так и Reaction. Он представляет собой любое значение или процесс, полученный из наблюдаемых данных.
Общие свойства:
Отслеживание зависимостей: оба отслеживают наблюдаемые данные, к которым они получают доступ во время выполнения, и становятся наблюдателями этих данных.
Управление устареванием: оба имеют свойство dependencyState_, указывающее, является ли их текущее значение актуальным или потенциально устаревшим в зависимости от изменений зависимостей.
Уведомление наблюдателя: оба объекта уведомляются наблюдаемыми объектами, когда их зависимости изменяются, что приводит к потенциальной повторной оценке или повторному выполнению.
Пример: Оба класса ComputedValue и Reaction реализуют интерфейс IDerivation, что означает, что они оба являются типами производных — значений или процессов, полученных из наблюдаемых данных.

Вычисленное значение

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

  • Одна из его зависимостей изменится.

  • Доступ к его значению осуществляется после изменения зависимости.

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

Пример: Класс ComputedValue в computedvalue.ts расширяет класс Atom и добавляет логику для вычисления значения на основе его зависимостей и запоминания результата. Он предоставляет метод get, который возвращает вычисленное значение и отслеживает зависимости, а также метод set (если предусмотрена функция установки) для обновления его внутреннего состояния.

Реакция

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

Ключевые характеристики:
Функционал реакции: определяет побочный эффект, который необходимо выполнить.

Пример: Класс Reaction в reaction.ts представляет собой побочный эффект, который реагирует на наблюдаемые изменения. Он хранит функцию реакции (onInvalidate_) и ее зависимости и планирует повторный запуск, когда его зависимости устаревают.

Эти концепции работают вместе для создания реактивной системы MobX, обеспечивающей эффективное и автоматическое обновление состояния приложения и побочных эффектов на основе изменений в базовых наблюдаемых данных.

Фундаментальные структуры

Map

Широко используется в библиотеке для различных целей:

Пример (хранение наблюдаемых значений): В observableobject.ts класс ObservableObjectAdministration использует Map (с именем values_) для хранения наблюдаемых свойств объекта.

Причины применения карты:
(1) Эффективное хранение ключей и значений
Карты обеспечивают эффективное хранение и извлечение значений ключей по сравнению с простыми объектами, особенно при работе с большим количеством свойств.
Ключом в Map является имя свойства, а значением — связанное ObservableValue или ComputedValue, которое управляет состоянием свойства и наблюдателями.

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

(3) Четкое отличие от обычных свойств
Использование отдельной карты для хранения наблюдаемых свойств четко отличает их от обычных ненаблюдаемых свойств целевого объекта. Такое разделение повышает ясность и позволяет избежать потенциальных конфликтов.

(4) Эффективное управление наблюдателями
При доступе к свойству или его изменении соответствующее ObservableValue или ComputedValue на карте обрабатывает взаимодействие с наблюдателем (отслеживание и уведомление).
Такое централизованное управление внутри карты упрощает управление наблюдателями и позволяет избежать добавления логики, связанной с наблюдателем, к каждому отдельному свойству.

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

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

Использование Map в ObservableObjectAdministration — это хорошо продуманный выбор дизайна, который сочетает в себе производительность, гибкость и удобство обслуживания, обеспечивая надежную основу для управления наблюдаемыми объектами.

Пример (внутреннее управление состоянием): Класс ObservableMap в observablemap.ts использует Map в data_ для хранения пар ключ-значение. Кроме того, он использует другую карту (hasMap_) для отслеживания существования определенного ключа и поддержания его статуса наблюдаемости.

Set

Используется для хранения коллекций уникальных элементов:

Пример (хранение наблюдателей): Классы Atom и ComputedValue используют Set (observers_) для отслеживания зависимых от них производных (реакций и вычисленных значений). Когда состояние наблюдаемого объекта изменяется, оно перебирает этот набор, чтобы уведомить каждого наблюдателя.

Пример (управление прослушивателями): ObservableArrayAdministration в observablearray.ts использует Set с именем changeListeners_ для хранения функций прослушивателя, которые заинтересованы в изменениях в наблюдаемом массиве.

Graph

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

Пример: Функция trackDerivedFunction в derivation.ts строит граф зависимостей, отслеживая, какие наблюдаемые данные доступны во время выполнения производной. Эта информация хранится в дереве newObserving_.

const newObserving_ = [{
  name_: 'My Observable1',
  observing_: [
    {
      name_: 'My Observable2',
      observing_: []
    }
  ]
  /* другие свойства */
}];

Вспомогательные структуры

Proxy Objects

Основная концепция: MobX использует прокси-объекты (если они доступны) для перехвата операций с наблюдаемыми объектами и массивами. Это позволяет прозрачно отслеживать изменения и уведомлять наблюдателей.
Пример: В dynamicobject.ts функция asDynamicObservableObject создает прокси вокруг простого объект. get, set и deleteProperty перехватывают доступ и изменения свойств прокси-сервера, позволяя MobX отслеживать изменения и реагировать соответствующим образом.

Property Descriptors

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

Алгоритмы

Отслеживание зависимостей и выполнение реакций

Batching

Основная концепция: Изменения наблюдаемых объектов группируются вместе, а реакции запускаются только один раз в конце пакета. Это сводит к минимуму ненужные вычисления и повышает производительность.
Пример: Функции startBatch и endBatch в observable.ts используются для групповых изменений в наблюдаемых данных. Реакции запускаются и выполняются только после вызова endBatch, что обеспечивает эффективное обновление.

Transaction Management

Основная концепция: Транзакции гарантируют, что все изменения внутри транзакции либо фиксируются, либо откатываются одновременно. Это обеспечивает согласованность данных и упрощает обработку ошибок.
Пример: Функция transaction в transaction.ts использует startBatch и endBatch для создания транзакционного контекста. Все изменения внутри транзакции группируются, и в случае возникновения ошибки состояние откатывается к предыдущим значениям.

Сравнение и равенство

Эталонное равенство

Основная концепция: По умолчанию для наблюдаемых значений используется сравнение на равенство ссылок (===). Это проверяет, относятся ли два значения к одному и тому же объекту в памяти.
Пример: В observablevalue.ts класс ObservableValue использует строгое равенство (===) по умолчанию для сравнения старых и новых значений в методе prepareNewValue_. Если значения одинаковы, он считает значение неизменным.

Структурное равенство

Основная концепция: MobX предоставляет утилиту comparer.structural, которая выполняет глубокое сравнение объектов и массивов. Это полезно в сценариях, где новые объекты с той же структурой не должны вызывать реакции.
Пример: Функция computed в computed.ts позволяет указать comparer.structural в качестве опции.

Неглубокое равенство

Пример: Подобно структурному сравнению, comparer.shallow можно использовать с вычисленными значениями и реакциями для выполнения поверхностного сравнения объектов и массивов, проверяя только первый уровень свойств.

Мемоизация

Основная концепция: Оптимизирует вызовы функций за счет кэширования результатов для одних и тех же входных данных, избегая избыточных вычислений.
Пример: Класс ComputedValue в computedvalue.ts кэширует свое вычисленное значение в свойство value_. Когда вызывается метод get, он проверяет, не является ли вычисленное значение устаревшим, на основе его зависимостей. Если нет, он возвращает кэшированное значение напрямую, повышая производительность.

Обработка ошибок

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

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

Утилиты и оптимизация

Генерация идентификаторов. MobX использует глобальные уникальные идентификаторы для определения производных и реакций, что помогает в отладке и оптимизации.
Предикаты типов. Такие функции, как isObservable и isComputed, помогают определить тип объекта и выполнить соответствующие действия.
Сборка мусора: MobX автоматически приостанавливает и очищает неиспользуемые наблюдаемые объекты для оптимизации использования памяти.

Шаблоны проектирования

Observer Pattern

Основная концепция: Определяет зависимость между объектами «один ко многим». Субъект (наблюдаемый) ведет список своих зависимых объектов (наблюдателей) и автоматически уведомляет их о любых изменениях состояния.

Пример: Класс ObservableValue в observablevalue.ts иллюстрирует это. Когда его метод set вызывается для обновления значения, он запускает метод reportChanged, который перебирает его observers_ (Set) и вызывает reportStale для каждого наблюдателя, уведомляя их об изменении.

Proxy Pattern

Основная концепция: Предоставляет суррогатный объект или объект-заполнитель, который управляет доступом к другому объекту, перехватывая операции и добавляя дополнительные функции.

Пример: Это демонстрирует функция asDynamicObservableObject в dynamicobject.ts. Он создает прокси вокруг простого объекта JavaScript. Этот прокси перехватывает такие операции, как get, set и deleteProperty, над свойствами объекта, позволяя MobX отслеживать изменения и реагировать соответствующим образом.

Decorator Pattern

Основная концепция: Динамическое добавление поведения или обязанностей к объекту без изменения его структуры.

Пример: MobX широко использует декораторы, такие как @observable, @computed и @action. В observable.ts декоратор observable берет свойство и оборачивает его в ObservableValue, делая его реактивным и интегрировав в систему отслеживания зависимостей.

Function Composition

Основная концепция: Объединяет простые функции для создания более сложных.

Пример: MobX позволяет создавать усилители для настройки наблюдаемого поведения. Например, вы можете применить к наблюдаемому объекту как «глубокие», так и «структурные» усилители, гарантируя глубокое структурное сравнение и предотвращая ненужные обновления, если структура объекта остается прежней.

Dependency Injection

Основная концепция: Предоставляет объекты с их зависимостями вместо их жесткого кодирования внутри объекта.

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

Publish-Subscribe Pattern

Основная концепция: Форма слабой связи, при которой издатели (наблюдаемые объекты) передают сообщения (изменения) подписчикам (наблюдателям), не зная, кто они.

Пример: Когда наблюдаемый объект, такой как ObservableValue, изменяется, он уведомляет своих наблюдателей (хранящихся в наборе observers_), эффективно «публикуя» событие об изменении. Затем наблюдатели реагируют на эту «публикацию», обновляя свое состояние или выполняя побочные эффекты.

Command Pattern (Action)

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

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

Factory Pattern

Пример: Хотя функция createAtom в atom.ts не используется напрямую в процессе создания наблюдаемых объектов, служит фабрикой для создания экземпляров Atom — основных единиц наблюдения в MobX.

Singleton

Объект globalState демонстрирует аспекты шаблона Singleton как единый, глобально доступный экземпляр, управляющий конфигурациями и состоянием MobX.

SOLID принципы

Принцип единой ответственности (SRP)

Пример (Atom): Класс Atom в atom.ts отвечает за хранение наблюдаемого состояния и управление взаимодействием наблюдателя. Он не обрабатывает реакции или побочные эффекты напрямую.

Пример (ComputedValue): Класс ComputedValue в computedvalue.ts фокусируется на логике вычислений и отслеживании зависимостей, оставляя реакции и побочные эффекты другим компонентам.

Пример (Реакция): Класс Reaction в reaction.ts Он отслеживает свои зависимости, планирует повторное выполнение при необходимости и управляет обработкой ошибок. Он не управляет напрямую наблюдаемыми данными.

Пример @observable): Декоратор @observable служит для обозначения объекта как наблюдаемого observable.ts. Он обрабатывает преобразование свойства в ObservableValue и интегрирует его в систему отслеживания зависимостей. Он не беспокоится о реакциях или побочных эффектах.

Принцип открытости-закрытости (OCP)

Модификаторы/усилители
Пример (deepEnhancer): Функция deepEnhancer в modifiers.ts позволяет создавать наблюдаемые объекты, которые позволяют также автоматически конвертировать вложенные объекты и массивы в наблюдаемые. Это расширяет поведение наблюдаемых объектов по умолчанию без изменения базовой реализации наблюдаемых объектов.

Реакция
Пример (автозапуск): Пользователи могут создавать собственные реакции с помощью функции autorun в autorun.ts, предоставляя функцию, определяющую логику реакции и ее зависимости. Это позволяет расширить функциональность реакций без изменения основного механизма реакции.

Принцип подстановки Лисков (LSP)

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

Принцип разделения интерфейсов (ISP)

Наблюдаемые интерфейсы
Пример: MobX определяет конкретные интерфейсы, такие как IObservableArray и IObservableMap, вместо одного общего интерфейса IObservable. Это позволяет каждому типу наблюдаемого объекта иметь собственный набор методов и свойств, соответствующих его структуре и поведению.

Перехватываемые и прослушиваемые интерфейсы
Пример: Интерфейсы IInterceptable и IListenable разделяют задачи перехвата изменений наблюдаемых объектов и прослушивания этих изменений. Это обеспечивает большую гибкость и модульность в использовании и расширении наблюдаемых объектов.

Принцип инверсии зависимостей (DIP)

Абстракции
Пример: Реакции и вычисляемые значения зависят от абстрактных интерфейсов IObservable и IDerivation, а не от конкретных наблюдаемых классов. Это обеспечивает гибкость и развязку, поскольку можно использовать различные наблюдаемые реализации, если они соответствуют интерфейсу.

Dependency Injection
Пример: Функция reaction в reaction.ts позволяет внедрять зависимости, например, пользовательскую функцию сравнения в реакции. Это способствует слабой связи и более легкому тестированию реакций.

© Habrahabr.ru