Нативная реализация JavaScript Система управления статусом STATE

JavaScript Vue.js React.js Redux

Build a state management system with vanilla JavaScript | CSS-Tricks

В программной инженерии управление состоянием не является новой концепцией, но более популярные фреймворки языка JavaScript используют родственные концепции. Традиционно мы сохраняем состояние самой DOM или даже объявляем это состояние как глобальную переменную. Но теперь у нас есть из чего выбирать. Например, Redux, MobX и Vuex упрощают управление состоянием компонентов. Это отлично работает для некоторых реактивных фреймворков, таких как React или Vue.

Однако как реализованы эти библиотеки управления состоянием? Можем ли мы создать его сами? Не обсуждая этого, по крайней мере, мы действительно можем понять общий механизм управления состоянием и некоторые популярные API.

Перед началом работы необходимы базовые знания JavaScript. Вы должны знать концепцию типов данных и понимать синтаксис и функции, связанные с ES6. Если ты многого не знаешь,иди сюда учиться. Эта статья не предназначена для замены Redux или MobX. Здесь мы проводим техническое исследование, и у каждого есть свое мнение.

предисловие

Прежде чем мы начнем, давайте взглянем на эффект, который должен быть достигнут.

Архитектурный дизайн

Используя вашу любимую IDE, создайте папку:

~/Documents/Projects/vanilla-js-state-management-boilerplate/

Структура проекта аналогична следующей:

/src
├── .eslintrc
├── .gitignore
├── LICENSE
└── README.md

Pub/Sub

Далее введитеsrcкаталог, создатьjsкаталог, созданный нижеlibкаталог и создатьpubsub.js.

Структура выглядит следующим образом:

/js
├── lib
└── pubsub.js

Открытьpubsub.jsпотому что мы собираемся реализоватьмодуль подписки/публикации. Полное название "Опубликовать/Подписаться". В нашем приложении мы создадим несколько функциональных модулей для подписки на наши именованные события. Другие модули публикуют соответствующие события, обычно применяемые к соответствующей последовательности загрузки.

Pub/Sub иногда сложно понять, как это смоделировать? Представьте, что вы работаете в ресторане, и у ваших пользователей есть лаунчер и меню. Если вы работаете на кухне, вы знаете, когда официант очистит пусковую установку (заказ), а затем сообщите шеф-повару, за каким столом очистили пусковую установку (заказ). Это цепочка заказов, соответствующая номеру стола. На кухне некоторым поварам нужно начинать работу. Они подписаны на эту ветку заказов, пока блюдо не будет готово, поэтому повар знает, что готовить. Таким образом, все повара под вашим началом готовят соответствующие блюда (называемые обратным вызовом) для одного и того же потока заказов (называемого событием).

Изображение выше является интуитивно понятным объяснением.

Модуль PubSub предварительно загрузит все подписки и выполнит соответствующие функции обратного вызова. Требуется всего несколько строк кода, чтобы создать очень элегантно отзывчивый поток.

существуетpubsub.jsДобавьте следующий код в:

export default class PubSub {
  constructor() {
    this.events = {};
  }
}

this.eventsИспользуется для сохранения определенных нами событий.

Затем добавьте следующий код в конструктор:

subscribe(event, callback) {

  let self = this;

  if(!self.events.hasOwnProperty(event)) {
    self.events[event] = [];
  }

  return self.events[event].push(callback);
}

Вот метод подписки. параметрevent— строковый тип, используемый для указания уникального имени события для обратного вызова. Если нет подходящего события вeventscollection, то мы создаем пустой массив для последующих проверок. Затем мы отправляем метод обратного вызова в эту коллекцию событий. Если есть коллекция событий, поместите функцию обратного вызова прямо в нее. Наконец, возвращает длину коллекции.

Теперь нам нужно получить соответствующий метод подписки, угадайте, что дальше?publishметод. Добавьте следующий код:

publish(event, data = {}) {

  let self = this;

  if(!self.events.hasOwnProperty(event)) {
    return [];
  }

  return self.events[event].map(callback => callback(data));
} 

Этот метод сначала проверяет, существует ли переданное событие. Возвращает пустой массив, если он не существует. Если он существует, переберите методы в коллекции и передайте в него данные для выполнения. Если метода обратного вызова нет, это тоже нормально, так как созданный нами пустой массив также будет работать дляsubscribeметод.

Это пабсаб. Смотри, что дальше!

Основной объект хранилища Store

Теперь, когда у нас есть модель подписки/публикации, мы хотим создать зависимость этого приложения: Store. Давайте посмотрим на это по крупицам.

Давайте посмотрим, для чего используется этот объект хранения.

Магазин является нашим основным объектом. каждый импорт@import store from '../lib/store.js', вы сохраните запрограммированные биты состояния в этом объекте. этоstate, содержащий все состояния нашего приложения, имеетcommitметод, который мы называемmutations, и, наконец, естьdispatchметод, который мы называемactions. Среди деталей этой основной реализации должна быть прокси-система для прослушивания и вещания наPubSubИзменения состояния в модели.

Создаем новую папкуstoreсуществуетjsпод.然后再创建一个store.jsдокумент. твойjsКаталог должен выглядеть так:

/js
└── lib
    └── pubsub.js
└──store
    └── store.js

Открытьstore.jsИ введите модуль подписки/публикации. следующим образом:

import PubSub from '../lib/pubsub.js';

Это распространено в синтаксисе ES6 и очень узнаваемо.

Далее приступаем к созданию объектов:

export default class Store {
  constructor(params) {
    let self = this;
  }
}

Вот самопровозглашение. Нам нужно создать по умолчаниюstate,actions,так же какmutations. мы хотим присоединитьсяstatusЭлементы используются для определения поведения объекта Store в любое время:

self.actions = {};
self.mutations = {};
self.state = {};
self.status = 'resting';

После этого нам нужно создать экземплярPubSub, связать нашStoreкакeventsэлемент:

self.events = new PubSub();

Далее нам нужно найти пройденныйparamsСодержит ли объектactionsилиmutations. когдаStoreПри инициализации мы передаем данные. содержитactionsа такжеmutationsСборник этой коллекции используется для управления сохраненными данными:

if(params.hasOwnProperty('actions')) {
  self.actions = params.actions;
}

if(params.hasOwnProperty('mutations')) {
  self.mutations = params.mutations;
}

Вышеуказанные наши настройки по умолчанию и возможные настройки параметров. Далее посмотримStoreКак объекты отслеживают изменения. мы будем использоватьProxyвыполнить. Прокси использует половину функциональности нашего объекта состояния. если мы используемget, каждый раз, когда к данным осуществляется доступ, они будут отслеживаться. тот же выборset, наш мониторинг будет действовать при изменении данных. код показывает, как показано ниже:

self.state = new Proxy((params.state || {}), {
  set: function(state, key, value) {

    state[key] = value;

    console.log(`stateChange: ${key}: ${value}`);

    self.events.publish('stateChange', self.state);

    if(self.status !== 'mutation') {
      console.warn(`You should use a mutation to set ${key}`);
    }

    self.status = 'resting';

    return true;
  }
});

в этотsetЧто происходит в функции? Это означает, что если есть изменения данных, такие какstate.name = 'Foo', этот код будет работать. Как раз вовремя в нашем контексте меняйте данные и печатайте. мы можем опубликоватьstateChangeсобытие дляPubSubмодуль. Функция обратного вызова любого подписанного события будет выполнена, мы проверяемStoreстатус, текущий статус должен бытьmutation, что означает, что состояние было обновлено. Мы можем добавить предупреждение, чтобы побудить разработчика неmutationРиск обновления данных в состоянии.

Отправка и коммитация

Мы добавили основные элементы вStoreХорошо, теперь мы добавляем два метода.dispatchдля исполненияactions,commitдля исполненияmutations. код показывает, как показано ниже:

dispatch (actionKey, payload) {

  let self = this;

  if(typeof self.actions[actionKey] !== 'function') {
    console.error(`Action "${actionKey} doesn't exist.`);
    return false;
  }

  console.groupCollapsed(`ACTION: ${actionKey}`);

  self.status = 'action';

  self.actions[actionKey](self, payload);

  console.groupEnd();

  return true;
}

Процесс следующий: найдите действие, если оно существует, установите статус и запустите действие.commitМетод очень похож.

commit(mutationKey, payload) {
  let self = this;

  if(typeof self.mutations[mutationKey] !== 'function') {
    console.log(`Mutation "${mutationKey}" doesn't exist`);
    return false;
  }

  self.status = 'mutation';

  let newState = self.mutations[mutationKey](self.state, payload);

  self.state = Object.assign(self.state, newState);

  return true;
}

Создайте базовый компонент

Создаем список для практики системы управления состоянием:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/lib/component.js
import Store from '../store/store.js';

export default class Component {
  constructor(props = {}) {
    let self = this;

    this.render = this.render || function() {};

    if(props.store instanceof Store) {
      props.store.events.subscribe('stateChange', () => self.render());
    }

    if(props.hasOwnProperty('element')) {
      this.element = props.element;
    }
  }
}

Давайте посмотрим на эту строку кода. Во-первых, представитьStoreДобрый. Нам не нужен экземпляр, но в него помещается больше проверок.constructorсередина. существуетconstructor, мы можем получить метод рендеринга, еслиComponentКласс является родительским классом других классов и может использовать унаследованные классы.renderметод. Если соответствующего метода нет, создается пустой метод.

После этого проверяемStoreсоответствие класса. нужно подтверждениеstoreпутьStoreЭкземпляр класса, если нет, не выполнять. Подписываемся на глобальную переменнуюstateChangeСобытия позволяют нашим программам реагировать. Метод рендеринга запускается каждый раз при изменении состояния.

Исходя из этого базового компонента, затем создаются другие компоненты.

Создадим наши компоненты

Создайте список:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/component/list.js
import Component from '../lib/component.js';
import store from '../store/index.js';

export default class List extends Component {

  constructor() {
    super({
      store,
      element: document.querySelector('.js-items')
    });
  }

  render() {
    let self = this;

    if(store.state.items.length === 0) {
      self.element.innerHTML = `<p class="no-items">You've done nothing yet &#x1f622;</p>`;
      return;
    }

    self.element.innerHTML = `
      <ul class="app__items">
        ${store.state.items.map(item => {
          return `
            <li>${item}<button aria-label="Delete this item">×</button></li>
          `
        }).join('')}
      </ul>
    `;

    self.element.querySelectorAll('button').forEach((button, index) => {
      button.addEventListener('click', () => {
        store.dispatch('clearItem', { index });
      });
    });
  }
};

Создайте компонент счетчика:

import Component from '../lib/component.js';
import store from '../store/index.js';

export default class Count extends Component {
  constructor() {
    super({
      store,
      element: document.querySelector('.js-count')
    });
  }

  render() {
    let suffix = store.state.items.length !== 1 ? 's' : '';
    let emoji = store.state.items.length > 0 ? '&#x1f64c;' : '&#x1f622;';

    this.element.innerHTML = `
      <small>You've done</small>
      ${store.state.items.length}
      <small>thing${suffix} today ${emoji}</small>
    `;
  }
}

Создайте компонент статуса:

import Component from '../lib/component.js';
import store from '../store/index.js';

export default class Status extends Component {
  constructor() {
    super({
      store,
      element: document.querySelector('.js-status')
    });
  }

  render() {
    let self = this;
    let suffix = store.state.items.length !== 1 ? 's' : '';

    self.element.innerHTML = `${store.state.items.length} item${suffix}`;
  }
}

Структура файлового каталога выглядит следующим образом:

/src
├── js
│   ├── components
│   │   ├── count.js
│   │   ├── list.js
│   │   └── status.js
│   ├──lib
│   │  ├──component.js
│   │  └──pubsub.js
└───── store
│      └──store.js
└───── main.js

Идеальное управление состоянием

У нас есть интерфейсные компоненты и основныеStore. Теперь требуется начальное состояние, некотороеactionsа такжеmutations. существуетstoreкаталог, создайте новыйstate.jsдокумент:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/store/state.js
export default {
  items: [
    'I made this',
    'Another thing'
  ]1
};

продолжать создаватьactions.js:

export default {
  addItem(context, payload) {
    context.commit('addItem', payload);
  },
  clearItem(context, payload) {
    context.commit('clearItem', payload);
  }
};

продолжать создаватьmutation.js

export default {
  addItem(state, payload) {
    state.items.push(payload);

    return state;
  },
  clearItem(state, payload) {
    state.items.splice(payload.index, 1);

    return state;
  }
};

последний созданныйindex.js:

import actions from './actions.js';
import mutations from './mutations.js';
import state from './state.js';
import Store from './store.js';

export default new Store({
  actions,
  mutations,
  state
});

окончательная интеграция

Наконец, мы интегрируем весь код вmain.jsв иindex.htmlсередина:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/main.js
import store from './store/index.js'; 

import Count from './components/count.js';
import List from './components/list.js';
import Status from './components/status.js';

const formElement = document.querySelector('.js-form');
const inputElement = document.querySelector('#new-item-field');

Теперь, когда все готово, добавьте взаимодействие ниже:

formElement.addEventListener('submit', evt => {
  evt.preventDefault();

  let value = inputElement.value.trim();

  if(value.length) {
    store.dispatch('addItem', value);
    inputElement.value = '';
    inputElement.focus();
  }
});

Добавьте визуализацию:

const countInstance = new Count();
const listInstance = new List();
const statusInstance = new Status();

countInstance.render();
listInstance.render();
statusInstance.render();

На сегодняшний день система государственного управления завершена.