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
— строковый тип, используемый для указания уникального имени события для обратного вызова. Если нет подходящего события вevents
collection, то мы создаем пустой массив для последующих проверок. Затем мы отправляем метод обратного вызова в эту коллекцию событий. Если есть коллекция событий, поместите функцию обратного вызова прямо в нее. Наконец, возвращает длину коллекции.
Теперь нам нужно получить соответствующий метод подписки, угадайте, что дальше?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 😢</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 ? '🙌' : '😢';
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();
На сегодняшний день система государственного управления завершена.