javascript

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

  • суббота, 2 ноября 2024 г. в 00:00:07
https://habr.com/ru/companies/gnivc/articles/855346/

Привет, меня зовут Дмитрий, я 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, так быстрее погружаешься в проект и понимаешь все зависимости и цепочки. Сейчас в моей разработке большой и сложный проект с кучей зависимостей в виде табличных данных, табличных фильтров, общих фильтров и сортировок. К сожалению, я не могу привести вам конкретный пример, потому что это коммерческая тайна. По своему опыту я не видел, чтобы разработчики пользовались этими инструментами. Но мне кажется, что иметь в арсенале своих скиллов эти инструменты определенно будет плюсом. Всем работающего кода, пока!