Скажем, у меня есть такой маленький объект
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 будет делать умные вещи, например отображать только те компоненты, которые подписаны на определенные отдельные биты данных в магазине, это действительно нечто особенное.