Скажем, у меня есть такой маленький объект

const state = {
  value: 12
}

Теперь скажем, у меня есть небольшая функция, подобная этой:

const render = (state) => {
  document.getElementById('root').innerHTML = 'count: ' + state.value
}

Теперь, когда я вызываю render(state), я вывожу текст «count: 12» в div с идентификатором «root». Очень хорошо. Измените состояние, вызовите рендеринг и мои обновления DOM.

Но это как-то отстойно. Что, если я хочу изменить более чем одну вещь? Должен ли я продолжать вызывать рендеринг? Этого никогда не будет.

Наблюдай и излучай

Давайте расширим состояние таким образом:

const state = {
  value: 12,
  observers: [],
  observe(fn) { 
    this.observers.push(fn);
  },
  emit() {
    this.observers.forEach(fn => fn(this));
  }
}

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

state.observe(render);
state.value = 13;
state.emit();

Видеть? Render теперь является обработчиком. Всякий раз, когда я вызываю emit, вызывается render. Я могу иметь столько функций, сколько захочу:

state.observe(({ value }) => console.log(value) );
state.emit()

Теперь мы не только рендерим DOM, но и делаем console.log.

Но это все еще отстой. Я должен помнить, чтобы вызывать emit каждый раз, когда я меняю состояние. Серьезно? В 2023 году? Мы можем исправить это с помощью геттеров и сеттеров.

Геттеры и сеттеры

Давайте изменим состояние следующим образом:

const state = {
  _value: 12,
  get value() {
    return this._value
  },
  set value(newValue) {
    const oldState = {...this}
    this._value = newValue;
    const newState = {...this}
    this.emit(newState, oldState);
  },

  observers: [],
  observe(fn) { 
    this.observers.push(fn);
  },
  emit() {
    this.observers.forEach(fn => fn(this));
  }
}

Оооо, что это? Геттер и сеттер? Посмотрите, как, когда я устанавливаю значение, мы составляем oldState и newState, а затем тут же вызываем emit.

Теперь это можно сделать:

state.value = 45

и наблюдатели будут вызваны. Больше ничего не требуется.

Прокси

Но ждать. Ведь нет необходимости вручную выписывать все эти геттеры и сеттеры, не так ли? Это точно было бы хреново. Также все массивы наблюдателей и дополнительные функции? К счастью, у нас есть встроенный объект JavaScript, специально разработанный для абстрагирования всего этого: прокси.

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

const createObservable = (target) => {
  const listeners = [];

  const subscribe = (listener) => listeners.push(listener);

  const emit = (oldTarget) => {
    listeners.forEach(listener => listener(target, oldTarget));
  }

  const store = new Proxy(
    target, 
    {
      set(target, property, value) {
        const oldTarget = {...target};
        target[property] = value;
        emit(oldTarget)
        return true;
      },
    }
  );

  return {
    store,
    subscribe,
  };
}

Этот фрагмент кода принимает цель (любой объект) и создает наблюдаемую версию этого объекта, перехватывая все атрибуты этого объекта и добавляя геттеры и сеттеры.

Мы называем это так:

const {store, subscribe} = createObservable({ cats: 45, tails: 90 })

Мы можем подписаться на него следующим образом:

subscribe((data, oldData) => console.log("Data was updated!", data, oldData))

Теперь, когда мы обновляем наш магазин, функции наблюдателя вызываются автоматически:

store.cats = 999

Больше ничего не требуется.

А это МОБКС

Некоторые из вас теперь будут кричать на свою машину, говоря: «Это MOBX!» и ты будешь прав. MobX — это библиотека, которая создаст наблюдаемое хранилище с использованием прокси. Это очень весело использовать, и я рекомендую вам попробовать его.

Интеграция MOBX React будет делать умные вещи, например отображать только те компоненты, которые подписаны на определенные отдельные биты данных в магазине, это действительно нечто особенное.