Как MobX делает объекты реактивными с помощью Proxy
- суббота, 22 марта 2025 г. в 00:00:03
Привет! На связи снова Дмитрий — React-разработчик, который стремится разобраться, как всё устроено, и делится информацией с вами.
В прошлой статье мы подробно разобрали, что такое Proxy и как он работает, поэтому повторяться не будем. Сегодня поговорим о том, как MobX использует Proxy, создавая свою "реактивную магию".
MobX превращает обычные объекты JavaScript в реактивные, что позволяет автоматически отслеживать изменения их свойств и обновлять зависимости. В основе этого механизма лежат два ключевых инструмента: makeAutoObservable и observable.
makeAutoObservable автоматически делает все свойства объекта наблюдаемыми, а геттеры - вычисляемыми, в свою очередь observable позволяет вручную определять, какие свойства должны быть наблюдаемыми.
В коде ниже makeAutoObservable(this) превращает count в observable, а increment() в action, благодаря этому MobX сможет отслеживать изменения и вызывать реакции при обновлении состояния.
import { makeAutoObservable, action, makeObservable, observable, runInAction } from 'mobx';
class Counter {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
}
const counter = new Counter();
Можно аналогичную функциональность получить другим способом:
import { observable, action } from "mobx";
const counter = observable({
count: 0,
increment: action(function () {
this.count++;
}),
});
Здесь count явно помечен как observable, а increment — как action. Такой подход дает больше контроля над тем, какие свойства становятся реактивными, но суть та же.
MobX использует Proxy, чтобы перехватывать операции get и set свойств объекта. Когда компонент React или другая часть приложения запрашивает observable-свойство, MobX фиксирует эту зависимость. Он оборачивает объекты в Proxy, чтобы автоматически отслеживать обращения к свойствам и их изменения.
Главной функцией MobX, как мы знаем, является способность автоматически отслеживать зависимости между состоянием и реакциями (например, компонентами React). Это позволяет MobX эффективно обновлять только те части интерфейса, которые зависят от изменившихся данных. Как же это работает под капотом? Основные механизмы MobX для отслеживания зависимостей - это reportObserved() и notifyObservers().
Когда компонент или другая часть приложения использует значение из наблюдаемого объекта, MobX должен запомнить, какое свойство этого объекта было использовано, чтобы правильно отслеживать его изменения. Это происходит с помощью функции reportObserved().
Каждый раз, когда свойство объекта читается, MobX вызывает reportObserved(). Эта функция добавляет текущую зависимость, например, компонент React или функцию, которая использует данные в список зависимостей данного свойства. Это позволяет понимать, какие части кода зависят от этого свойства и автоматически обновлять их при изменении значения.
Примерная реализация reportObserved() может выглядеть так:
function reportObserved(property) {
const observers = getObserversForProperty(property);
const currentReaction = getCurrentReaction();
if (currentReaction && observers) {
observers.add(currentReaction);
}
}
getObserversForProperty(property) - функция, которая получает список зависимостей для данного свойства. Для каждого наблюдаемого свойства в MobX есть свой список наблюдателей, которые должны быть уведомлены об изменении.
getCurrentReaction() - функция, которая получает реакцию (например, компонент React или вычисление), которая использует это свойство. Она нужна для того, чтобы понять, кто сейчас "слушает" это свойство.
Внутри reportObserved() мы добавляем текущую реакцию в список наблюдателей для этого свойства, чтобы в дальнейшем обновить только те части системы, которые зависят от изменения этого свойства.
Когда происходит изменение наблюдаемого значения, MobX должен уведомить все зависимости из составленного списка. Для этого используется метод notifyObservers().
Примерная реализация notifyObservers() может выглядеть так:
function notifyObservers(property) {
const observers = getObserversForProperty(property);
if (observers) {
observers.forEach(observer => {
observer.update();
});
}
}
getObserversForProperty(property) - возвращает список наблюдателей, зарегистрированных для этого свойства.
observer.update() - для каждого наблюдателя (например, компонента или вычисления), который зависит от этого свойства, вызывается метод обновления. Это может быть перерисовка компонента или пересчёт вычисленного значения.
Когда вы работаете с наблюдаемым объектом MobX, цикл взаимодействия этих функций следующий:
При чтении значения свойства вызывается reportObserved(), которая добавляет текущую реакцию в список зависимостей этого свойства.
При изменении значения свойства (например, через set) вызывается notifyObservers(), которая уведомляет все зависимости об изменении и вызывает обновление реакций.
import { observable, autorun } from 'mobx';
const store = observable({
count: 0
});
// autorun будет отслеживать все изменения в store.count
autorun(() => {
console.log(store.count);
});
store.count = 10; // Это вызывает notifyObservers и обновит autorun
Когда store.count изменяется, MobX вызывает notifyObservers(), и autorun автоматически перезапускается, выводя новое значение.
Да, reportObserved() и notifyObservers() - это внутренние функции. Они не являются частью публичного API, но играют ключевую роль в механизме реактивности библиотеки.
Обе эти функции работают внутри MobX, чтобы автоматически поддерживать реактивность, не требуя от разработчика вручную управлять зависимостями или обновлениями. Они являются частью низкоуровневой реализации MobX и вряд ли будут вызываться напрямую в коде. Вместо этого, MobX использует их в своем процессе реактивности, чтобы "слушать" изменения состояния и обновлять зависимости.
Когда MobX делает объект реактивным, он оборачивает его в Proxy, который перехватывает доступ к свойствам и изменения. Однако при деструктуризации const { count } = store происходит следующее:
count получает примитивное число (например, 0), а не ссылку на реактивный объект, а примитивы не могут быть реактивными. В MobX реактивность работает только на уровне объектов (через Proxy). Когда count копируется в отдельную переменную, он теряет связь с store. Соответсвенно отсутствуют get-ловушки Proxy. MobX не может перехватить get, потому что count уже не часть проксифицированного объекта, а просто число.
import { observable, autorun } from "mobx";
const store = observable({
count: 0
});
autorun(() => {
console.log("Count changed:", store.count);
});
const { count } = store; // Деструктуризация
console.log(count); // 0
store.count = 10; // Выводит: Count changed: 10
console.log(count); // Все еще 0, потому что не отслеживается
Как избежать проблемы? Всегда обращаться к store.count, чтобы MobX мог перехватывать get, или использовать computed (если значение нужно часто)
"Но я же использую деструктуризацию стора в своих компонентах, и там все работает!" - скажете вы. Да, все верно, если store используется в функциональном компоненте React, и он обернут в observer, MobX все равно корректно отслеживает обновления. Главное - не забывать оборачивать компонент в observer.
import { observer } from 'mobx-react-lite';
const Counter = observer(({ store }) => {
const { count } = store; // Несмотря на деструктуризацию, компонент обновится
return <div>{count}</div>;
});
Здесь observer следит за компонентом в целом. При изменении store.count MobX автоматически перерисует Counter, даже если count был деструктурирован.
В заключение хочу подытожить: я старался простым языком объяснить, как работает прокси в MobX, не углубляясь в слишком сложные детали. Кажется, мы охватили все ключевые моменты, чтобы сложилось представление о всем этом процессе и его механизмах.