Отладка и мониторинг в MobX: trace, introspection и spy

от автора

Привет, меня зовут Дмитрий, я 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *