Управление состоянием — одна из наиболее важных частей веб-приложения. От использования глобальных переменных для перехватчиков React до использования сторонних библиотек, таких как MobX, Redux или XState, если назвать только эти три, — это одна из тем, которая вызывает больше всего дискуссий, поскольку важно освоить ее для разработки надежное и эффективное приложение.
Сегодня я предлагаю создать мини-библиотеку управления состоянием менее чем в 50 строк JavaScript на основе концепции наблюдаемых. Его, безусловно, можно использовать как есть для небольших проектов, но помимо этого образовательного упражнения я все же рекомендую вам обратиться к более стандартизированным решениям для ваших реальных проектов.
При запуске нового проекта библиотеки важно с самого начала определить, как может выглядеть его API, чтобы заморозить его концепцию и направлять ее разработку, прежде чем даже думать о деталях технической реализации. Для реального проекта уже сейчас можно даже начать писать тесты для проверки реализации библиотеки, написанной в соответствии с подходом TDD.
Здесь мы хотим экспортировать один класс, который мы назовем State, который будет создан с помощью объекта, содержащего начальное состояние, и одного метода наблюдения, который позволяет нам подписываться на изменения состояния с помощью наблюдателей. Эти наблюдатели должны выполняться только в том случае, если одна из их зависимостей изменилась.
Чтобы изменить состояние, мы хотим использовать свойства класса напрямую, а не использовать такой метод, как setState.
Поскольку фрагмент кода стоит тысячи слов, вот как может выглядеть наша окончательная реализация:
const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 1 // Text changed Hello, world! // Count changed 2
Давайте начнем с создания класса State, который принимает начальное состояние в своем конструкторе и предоставляет метод наблюдения, который мы реализуем позже.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; } observe(observer) { this.observers.push(observer); } }
Здесь мы решили использовать внутренний промежуточный объект состояния, который позволит нам сохранять значения состояния. Мы также храним наблюдателей во внутреннем массиве наблюдателей, который будет полезен, когда мы завершим эту реализацию.
Поскольку эти два свойства будут использоваться только внутри этого класса, мы могли бы объявить их частными с небольшим синтаксическим сахаром, добавив к ним префикс # и добавив первоначальное объявление в классе:
class State { #state = {}; #observers = []; constructor(initialState = {}) { this.#state = initialState; this.#observers = []; } observe(observer) { this.#observers.push(observer); } }
В принципе, это было бы хорошей практикой, но на следующем этапе мы будем использовать прокси, а они несовместимы с частными объектами. Не вдаваясь в подробности и чтобы упростить эту реализацию, мы пока будем использовать общедоступные свойства.
Когда мы обрисовывали спецификации для этого проекта, мы хотели получить доступ к значениям состояния непосредственно в экземпляре класса, а не как запись в его внутреннем объекте состояния.
Для этого мы будем использовать прокси-объект, который будет возвращен при инициализации класса.
Как следует из названия, прокси позволяет вам создать посредника для объекта для перехвата определенных операций, включая его методы получения и установки. В нашем случае мы создаем прокси, предоставляющий первый метод получения, который позволяет нам предоставлять входные данные объекта состояния, как если бы они принадлежали непосредственно экземпляру состояния.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0
Теперь мы можем определить объект начального состояния при создании экземпляра State, а затем получать его значения непосредственно из этого экземпляра. Теперь давайте посмотрим, как манипулировать его данными.
Мы добавили метод получения, поэтому следующим логическим шагом будет добавление метода установки, позволяющего нам манипулировать объектом состояния.
Сначала мы проверяем, что ключ принадлежит этому объекту, затем проверяем, что значение действительно изменилось, чтобы предотвратить ненужные обновления, и, наконец, обновляем объект новым значением.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0 state.count = 1; console.log(state.count); // 1
Теперь мы закончили с чтением и записью данных. Мы можем изменить значение состояния, а затем получить это изменение. Пока наша реализация не очень полезна, поэтому давайте реализуем наблюдателей сейчас.
У нас уже есть массив, содержащий функции-наблюдатели, объявленные в нашем экземпляре, поэтому все, что нам нужно сделать, это вызывать их одну за другой при каждом изменении значения.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach((observer) => { observer(this.state); }); } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; // Output: // Count changed 1 // Text changed // Count changed 1 // Text changed Hello, world!
Отлично, мы реагируем на изменения данных!
Однако небольшая проблема. Если вы до сих пор обращали внимание, мы изначально хотели запускать наблюдателей только в том случае, если изменится одна из их зависимостей. Однако, если мы запустим этот код, мы увидим, что каждый наблюдатель запускается каждый раз, когда изменяется часть состояния.
Но тогда как мы можем определить зависимости этих функций?
И снова нам на помощь приходят прокси. Чтобы определить зависимости наших функций-наблюдателей, мы можем создать прокси нашего объекта состояния, запустить их с ним в качестве аргумента и отметить, к каким свойствам они обращались.
Просто, но эффективно.
При вызове наблюдателей все, что нам нужно сделать, это проверить, есть ли у них зависимость от обновленного свойства, и активировать их только в этом случае.
Вот окончательная реализация нашей мини-библиотеки с добавленной последней частью. Вы заметите, что массив наблюдателей теперь содержит объекты, позволяющие сохранять зависимости каждого наблюдателя.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach(({ observer, dependencies }) => { if (dependencies.has(prop)) { observer(this.state); } }); } } else { target[prop] = value; } }, }); } observe(observer) { const dependencies = new Set(); const proxy = new Proxy(this.state, { get: (target, prop) => { dependencies.add(prop); return target[prop]; }, }); observer(proxy); this.observers.push({ observer, dependencies }); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.observe((state) => { console.log('Count or text changed', state.count, state.text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 0 // Text changed // Count or text changed 0 // Count changed 1 // Count or text changed 1 // Text changed Hello, world! // Count or text changed 1 Hello, world! // Count changed 2 // Count or text changed 2 Hello, world!
И вот, в 45 строках кода мы реализовали мини-библиотеку управления состоянием на JavaScript.
Если бы мы хотели пойти дальше, мы могли бы добавить предложения типов с помощью JSDoc или переписать это на TypeScript, чтобы получать предложения по свойствам экземпляра состояния.
Мы также могли бы добавить метод unobserve, который будет доступен для объекта, возвращаемого State.observe.
Также может быть полезно абстрагировать поведение установщика в метод setState, который позволяет нам изменять несколько свойств одновременно. В настоящее время нам приходится изменять каждое свойство нашего состояния одно за другим, что может вызвать срабатывание нескольких наблюдателей, если некоторые из них имеют общие зависимости.
В любом случае, я надеюсь, что вам понравилось это небольшое упражнение так же, как и мне, и что оно позволило вам немного глубже углубиться в концепцию прокси-сервера в JavaScript.
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3