Привет, меня зовут Дмитрий, я Middle-React-разработчик с замашками сеньора, поднимающийся с самых низов без мам, пап и ипотек. В последнее время я частенько вижу ситуацию: при использовании MobX в больших проектах у людей появляются сложности с количеством перерисовок или наоборот не обновлением данных со стора. Также могут проявляться проблемы с производительностью в том числе и из-за этого. Я решил поделиться отладочными инструментами MobX, ведь это может кому пригодиться.
Реактивное программирование и состояние в MobX
Немного справочной информации про концепцию реактивного программирования. Реактивное программирование — это концепция, где данные и действия синхронизируются автоматически. Если что-то меняется в одном месте, это изменение каскадно влияет на все связные элементы приложения. MobX помогает превратить объекты JS в структуры данных, которые отслеживают изменения и обновляют приложение в реальном времени.
Основными инструментами MobX являются:
-
Observable (наблюдаемые): свойства, которые реагируют на изменения данных.
-
Computed (вычисляемые): зависимости, которые пересчитываются только при необходимости.
-
Reactions (реакции): побочные эффекты, выполняемые в ответ на изменения в наблюдаемых данных.
Используя эти элементы, MobX создает реактивные цепочки. Изменения в observables автоматически вызывают пересчет зависимых computed и реакций, что позволяет минимизировать ручное управление состоянием.
Но когда зависимостей и переменных становится много, бывает тяжело понять, откуда что обновилось и что вызвало перерендер. Для решения такой проблемы существуют инструменты отладки, о которых я постараюсь рассказать.
Под капотом
Одной из ключевых особенностей MobX, начиная с версии 5, является использование прокси-объектов, которые позволяют эффективно перехватывать изменения в состоянии и управлять зависимостями между данными и реакциями.
MobX создает реактивные объекты через прокси, что позволяет «перехватывать» каждое обращение к свойствам этих объектов. Этот механизм дает возможность MobX отслеживать зависимости между данными в реальном времени, как только мы обращаемся к какому-либо свойству. Функции MobX:
-
Создание наблюдаемых свойств: MobX оборачивает объекты в прокси, отслеживая доступ ко всем их свойствам и регистрируя зависимости. Например, если в реакции используется свойство объекта, MobX автоматически добавляет это свойство в зависимости реакции.
-
Инвалидация при изменениях: Прокси позволяют MobX сразу же узнавать, когда какое-то наблюдаемое значение изменяется. Если в MobX-объекте обновляется свойство, то через прокси MobX «инвалидирует» все вычисляемые значения и реакции, которые зависят от этого свойства, и автоматически пересчитывает их.
-
Простота управления состоянием: Благодаря прокси, MobX не требует дополнительных обёрток для каждого свойства. Все новые свойства, которые добавляются к объектам, также сразу становятся реактивными, что делает MobX простым в использовании.
Использование прокси позволило MobX упростить и оптимизировать реактивность, так как только зависимости, затронутые изменениями, пересчитываются. Это увеличивает производительность и снижает потребление ресурсов, обеспечивая плавное обновление интерфейса при изменении данных.
Зачем нужны инструменты для отладки и мониторинга?
MobX требует контроля со стороны разработчика: необходимо убедиться, что реактивные связи и состояния обновляются только тогда, когда это действительно необходимо. Если зависимости не настроены корректно, это может привести к ненужным пересчетам и обновлениям, что может негативно сказаться на производительности. Кроме того, ошибки, связанные с реактивностью, не всегда очевидны и могут проявляться в неожиданном поведении.
С помощью таких инструментов, как trace, introspection и spy, разработчик получает возможность следить за всем, что происходит в реактивных недрах проекта, и использовать MobX максимально эффективно.
Отладка может быть сложной задачей, т. к. требуется не только понять, какие изменения происходят, но и выяснить, что именно инициирует их. Давайте начнем с инструмента trace().
Что такое trace и как он работает?
Trace() в MobX — это встроенный метод для отслеживания реактивных связей. Когда мы вызываем trace() внутри функции autorun или computed, MobX выводит детальную информацию о том, какие именно зависимости вызвали её пересчёт. Также trace() можно вызвать и просто в компоненте и если запустим приложение, то в момент обновления переменной получим точку дебага в браузере. Это полезно для ситуаций, когда вы пытаетесь понять, почему какое-то вычисляемое значение или реакция обновляется чаще, чем ожидается.
Trace() позволяет увидеть, какие observable свойства участвуют в вычислении. Это помогает идентифицировать потенциальные проблемы с лишними перерисовками. С помощью этой информации можно более эффективно управлять состоянием и устранять неочевидные ошибки в реактивной логике.
Примеры использования trace
Рассмотрим пример, где trace помогает разобраться в работе autorun и computed. Предположим, у нас есть класс Store с наблюдаемыми свойствами a и b, а также вычисляемое свойство sum.
import { autorun, trace, makeAutoObservable } from "mobx"; class Store0 { a = 10; b = 20; constructor() { makeAutoObservable(this); } // Сеттер для свойства a setA(value) { this.a = value; } // Вычисляемое свойство sum get sum() { trace(); // Включаем trace внутри computed return this.a + this.b; } } const store0 = new Store0(); // Создаем autorun для автоматического обновления при изменении sum autorun(() => { trace(); // Включаем trace внутри autorun console.log("Сумма:", store0.sum); }); export default store0;
А также я создал компонент для вывода и изменения переменных.
import React from "react"; import { observer } from "mobx-react-lite"; import store0 from "./Store0"; const Store0Component = observer(() => { const handleAChange = (event) => { const newA = parseInt(event.target.value, 10); store0.setA(isNaN(newA) ? 0 : newA); }; return ( <div> <h2>Store0 Variables</h2> <p>Value of a: {store0.a}</p> <p>Value of b: {store0.b}</p> <p>Sum (a + b): {store0.sum}</p> <h3>Update Value of a</h3> <label> a: <input type="number" value={store0.a} onChange={handleAChange} /> </label> </div> ); }); export default Store0Component;
В этом примере, когда значения a или b изменяются, computed sum- свойство пересчитывается, и autorun вызывается для вывода нового значения суммы. Благодаря trace() MobX будет выводить информацию о том, какие зависимости (в данном случае a и b) привели к пересчету.
Теперь представим, что мы изменяем значение a:
Вот что выведет в консоль.
Сообщения, которые выводит spy
и trace
, помогают понять последовательность изменений и реакций в Store0
. Давайте разберём каждое событие:
Spy event — action:
Spy event: {type: 'action', name: 'setA', object: Store0, arguments: Array(1), spyReportStart: true}
Это означает, что был вызван метод setA
(названный «action» в MobX). Он изменяет значение a
и запустит отслеживание, когда его вызвали. Сообщение spyReportStart: true
указывает на начало выполнения setA
.
Spy event — update (observable a):
Spy event: {type: 'update', observableKind: 'object', debugObjectName: 'Store0@5', object: Store0, oldValue: 10, newValue: 11}
После вызова setA
, MobX зафиксировал изменение в a
: старое значение (oldValue
) было 10
, а новое значение (newValue
) стало 11
. Это событие обновления наблюдаемого объекта a
.
MobX trace — computed sum:
[mobx.trace] 'Store0@5.sum' is invalidated due to a change in: 'Store0@5.a'
Здесь trace
показывает, что вычисляемое значение sum
стало «невалидным» из-за изменения a
. Это означает, что MobX
понимает, что sum
нужно пересчитать, поскольку оно зависит от a
.
Spy event — computed update (sum):
Spy event: {observableKind: 'computed', debugObjectName: 'Store0@5.sum', object: Store0, type: 'update', oldValue: 30, newValue: 31}
После пересчета sum
его значение изменилось с 30
на 31
, и это зафиксировано как обновление вычисляемого свойства.
MobX trace — autorun:
[mobx.trace] 'Autorun@6' is invalidated due to a change in: 'Store0@5.sum'
trace
показывает, что autorun
был запущен снова, поскольку sum
обновился. autorun
пересчитывается каждый раз, когда sum
(или любое наблюдаемое свойство внутри него) изменяется.
Spy event — reaction (autorun):
Spy event: {name: 'Autorun@6', type: 'reaction', spyReportStart: true}
MobX зафиксировал, что autorun
запускается как реакция на обновление sum
, и реактивный код выполняется снова.
Console output — Updated sum:
Сумма: 31
Это вывод из autorun
, который печатает новое значение sum
в консоль.
Когда использовать trace?
trace полезен в следующих случаях:
-
Отладка неожиданных обновлений и улучшение производительности. Когда какое-то computed свойство или autorun обновляется чаще, чем нужно, trace помогает понять, какая зависимость инициирует эти обновления.
-
Анализ сложных зависимостей. В больших приложениях с множеством взаимозависимых observable и computed свойств trace помогает визуализировать связи, что упрощает понимание для разработчика.
С помощью trace можно быстро выявлять и устранять лишние перерендеры и получать полное представление о том, как именно MobX управляет реактивными зависимостями в приложении.
Introspection в MobX
Есть еще один инструмент для анализа. Introspection дает разработчику возможность заглянуть внутрь реактивных объектов и получить информацию о структуре зависимостей, типах свойств и текущем состоянии каждого из них.
Что такое introspection и какие методы предоставляет MobX?
Introspection— это набор методов, которые позволяют анализировать и получать доступ к информации о реактивных объектах, таких как observable свойства и computed значения. Эти методы помогают выяснить, является ли объект наблюдаемым, получить список его зависимостей и определить, какие элементы участвуют в вычислениях. Возможность использовать introspection важна для более глубокого понимания структуры реактивных цепочек и диагностики проблем в больших приложениях.
Основные методы introspection в MobX:
-
isObservable — проверяет, является ли объект или его свойство наблюдаемым.
-
isComputedProp — определяет, является ли свойство вычисляемым (computed).
-
getDependencyTree — отображает структуру зависимостей для реактивного объекта, что позволяет понять, какие observable или computed свойства влияют на его значение.
-
getObserverTree — показывает, кто «подписан» на данный объект, т. е. какие реакции или вычисления зависят от него.
Эти методы дают более полное представление о том, как устроены и взаимодействуют друг с другом различные элементы реактивного состояния.
Примеры использования introspection в сложных структурах
В качестве примера я придумал более сложный стор, чем в предыдущем примере. У него есть вложенные объекты, несколько уровней наблюдаемых свойств и вычисляемых значений. Версия MobX в данном примере «mobx-react-lite»: «^4.0.7». Обращайте на это внимание, потому что синтаксис со старыми версиями, где были декораторы отличается.
import { makeAutoObservable } from "mobx"; class Store { user = { name: "Alice", age: 30, settings: { theme: "dark", notifications: true, }, }; activities = [ { title: "Jogging", duration: 30 }, { title: "Coding", duration: 120 }, ]; constructor() { makeAutoObservable(this); } get totalActivityDuration() { return this.activities.reduce( (sum, activity) => sum + activity.duration, 0 ); } get userInfo() { return `${this.user.name}, Age: ${this.user.age}`; } } const store = new Store(); export default store;
Также я создал компонент MyComponent, в котором мы хотим проверить, что от чего зависит и наблюдается ли.
import React, { useEffect } from "react"; import { observer } from "mobx-react-lite"; import { isObservable, isComputedProp, getDependencyTree } from "mobx"; import store from "./Store"; const MyComponent = observer(() => { useEffect(() => { // Проверка, является ли user наблюдаемым объектом console.log("user is observable:", isObservable(store.user)); // true console.log( "user.settings is observable:", isObservable(store.user.settings) ); // true // Проверка, является ли totalActivityDuration вычисляемым console.log( "totalActivityDuration is computed:", isComputedProp(store, "totalActivityDuration") ); // true // Получение дерева зависимостей для userInfo console.log( "Dependency tree for userInfo:", getDependencyTree(store, "userInfo") ); // Получение дерева зависимостей для totalActivityDuration console.log( "Dependency tree for totalActivityDuration:", getDependencyTree(store, "totalActivityDuration") ); }, []); return ( <div> <h1>User Info: {store.userInfo}</h1> <p>Total Activity Duration: {store.totalActivityDuration}</p> </div> ); }); export default MyComponent;
Тогда при запуске MyComponent в консоль выведется следующее:
По выводу мы сразу понимаем, что наблюдаемо, а также видим структуру. Думаю, комментарии излишни.
Spy в MobX
Иногда для полноценного понимания работы реактивного состояния требуется видеть все события, происходящие в MobX, в реальном времени. Именно для этого и служит инструмент spy. Это отладочный инструмент, который отслеживает все изменения в MobX и выводит их в консоль. Использование spy позволяет буквально заглянуть «под капот» реактивной системы и увидеть, какие действия происходят в каждый момент времени, будь то изменение observable, запуск computed функции или выполнение эффекта.
Что такое spy и как он работает?
spy — это метод, который позволяет подписаться на все события, происходящие в MobX. С помощью spy можно отслеживать любые изменения состояния и действий, таких как обновления наблюдаемых значений, пересчёты вычисляемых свойств и срабатывание реакций.
Каждое событие, отслеживаемое spy, включает тип события (например, «update» для изменения значения observable или «compute» для пересчета computed), а также дополнительную информацию, такую как старое и новое значения для observable свойств. Это позволяет видеть полную картину изменений в приложении и обнаруживать неожиданные действия или неэффективные пересчёты.
Примеры использования spy
Изменим наш предыдущий store, добавив геттеры и сеттеры, чтобы можно было менять значение из компонента.
import { makeAutoObservable } from "mobx"; class Store2 { user = { name: "Alice", points: 100, status: "active", }; levelMultiplier = 2; constructor() { makeAutoObservable(this); } // Сеттер для user.points setUserPoints(points) { this.user.points = points; } // Сеттер для levelMultiplier setLevelMultiplier(multiplier) { this.levelMultiplier = multiplier; } // Вычисляемое свойство: базовый уровень get baseLevel() { return Math.floor(this.user.points / 100); } // Вычисляемое свойство: уровень с учетом множителя get adjustedLevel() { return this.baseLevel * this.levelMultiplier; } // Вычисляемое свойство: статус игрока в зависимости от уровня get userStatus() { return this.adjustedLevel > 5 ? "VIP" : this.user.status; } } const store2 = new Store2(); export default store2;
Сделаем новый компонент для примера StoreComponent. Который будет отображать и изменять наши переменные в Store2. И подключим spy, чтобы увидеть изменения в реактивной цепочке, которая включает наблюдаемые и вычисляемые свойства.
import React from "react"; import { observer } from "mobx-react-lite"; import store2 from "./Store2"; import { spy } from "mobx"; const StoreComponent = observer(() => { spy((event) => { console.log('Spy event:', event); }); const handlePointsChange = (event) => { const points = parseInt(event.target.value, 10); store2.setUserPoints(isNaN(points) ? 0 : points); }; const handleMultiplierChange = (event) => { const multiplier = parseInt(event.target.value, 10); store2.setLevelMultiplier(isNaN(multiplier) ? 1 : multiplier); }; return ( <div> <h2>User Info</h2> <p>Name: {store2.user.name}</p> <p>Points: {store2.user.points}</p> <p>Status: {store2.user.status}</p> <h2>Levels</h2> <p>Base Level: {store2.baseLevel}</p> <p>Adjusted Level: {store2.adjustedLevel}</p> <p>User Status: {store2.userStatus}</p> <h2>Adjust Points and Level Multiplier</h2> <div> <label> Points: <input type="number" value={store2.user.points} onChange={handlePointsChange} /> </label> </div> <div> <label> Level Multiplier: <input type="number" value={store2.levelMultiplier} onChange={handleMultiplierChange} /> </label> </div> </div> ); }); export default StoreComponent;
Запускаем и прям из интерфейса давайте поменяем значение points или levelMultiplier и посмотрим, какие события будут зафиксированы при изменении points на 101 и levelMultiplier, например на 3;
В консоле мы увидим для store.user.points структуру логов вида:
Строк много и поначалу, кажется, что ничего не понятно. Спешу вас разубедить:) В целом, по этим логам можно будет понять, что было событие update для user.points, которое, изменило значение с 100 на 101. Это изменение инициирует пересчет baseLevel, так как baseLevel зависит от user.points. Затем adjustedLevel также пересчитывается, поскольку он зависит от baseLevel и levelMultiplier. Наконец, userStatus пересчитывается, поскольку его значение зависит от adjustedLevel.
И для store.levelMultiplier будет похожая структура, по которой будет видно изменение значения levelMultiplier с 2 на 3. Это изменение инициирует пересчет adjustedLevel, т.к. он зависит от levelMultiplier. После этого userStatus пересчитывается, так как его значение зависит от adjustedLevel.
Но будьте осторожны, в консоль может высыпаться столько, что все повиснет.
Когда использовать spy?
Инструмент spy оказывается особенно полезен в следующих ситуациях:
-
Поиск неожиданных изменений состояния.
-
Оптимизация реактивных цепочек.
-
Обнаружение побочных эффектов.
Включение spy может дать разработчику полное представление о том, что происходит внутри MobX, позволяя не только находить ошибки, но и выявлять и устранять возможные проблемы с производительностью.
Заключение
MobX предоставляет разработчикам инструменты для отладки и мониторинга, такие как trace
, introspection
и spy
. Каждый из этих инструментов выполняет свою роль и помогает понять, как работает реактивное состояние и какие зависимости его формируют. Так чем же они отличаются и в каких ситуациях их использовать?
Сравнение trace, introspection и spy
-
Trace: фокусируется на реактивных зависимостях. Этот инструмент помогает видеть, какие observable свойства вызывают пересчёт
computed
значений и реакций. Лучше всего подходит для анализа и оптимизации конкретных цепочек реактивных обновлений, когда нужно понять, что вызывает пересчёт и почему. -
Introspection: служит для анализа структуры реактивных объектов. С его помощью можно определить, является ли свойство observable или computed, а также получить дерево зависимостей или наблюдателей для определённых значений. Инструмент особенно полезен для детального анализа состояния и выявления зависимостей в крупных проектах.
-
Spy: глобальный инструмент для мониторинга всех событий в MobX. Отслеживает любые изменения в observable, computed пересчёты и реакции в реальном времени, выводя в консоль каждое событие. Полезен для получения полной картины состояния и поиска неожиданных изменений или избыточных пересчётов.
Примеры, в каких случаях какой инструмент предпочтительнее использовать
-
Если вы хотите понять, почему конкретный
computed
значение пересчитывается, используйтеtrace
. Он покажет, какие зависимости инициировали пересчёт и поможет устранить лишние вызовы. -
Если необходимо узнать структуру объекта и его зависимости, используйте
introspection
. Это поможет вам увидеть взаимосвязи внутри реактивного состояния и, возможно, оптимизировать его структуру. -
Если нужно отследить все изменения в приложении в реальном времени или найти источник неожиданных изменений, используйте
spy
. Он полезен для отладки сложных систем и помогает идентифицировать потенциальные узкие места или побочные эффекты.
Из опыта:
Переходя на новый проект, в период знакомства и погружения я, периодически, использую trace, если проект несильно замудреный и реже, скорее, прям очень редко, приходится юзать spy, так быстрее погружаешься в проект и понимаешь все зависимости и цепочки. Сейчас в моей разработке большой и сложный проект с кучей зависимостей в виде табличных данных, табличных фильтров, общих фильтров и сортировок. К сожалению, я не могу привести вам конкретный пример, потому что это коммерческая тайна. По своему опыту я не видел, чтобы разработчики пользовались этими инструментами. Но мне кажется, что иметь в арсенале своих скиллов эти инструменты определенно будет плюсом. Всем работающего кода, пока!
ссылка на оригинал статьи https://habr.com/ru/articles/855346/
Добавить комментарий