Обновление внешнего вида на основе состояния обычно выносят в отдельный слой, который называется представлением (View). В простейшем случае представлением является функция, которая принимает на вход состояние, анализирует его и производит необходимые изменения в DOM.
// Лучше вынести в отдельный файл, так как обычно там много кода
import render from './view.js';
const state = /* данные состояния */;
const input = document.querySelector(/* .... */);
input.addEventListener('keyup', (e) => {
state.registrationForm.value = e.target.value;
if (input.value === '' || input.value.match(/^\d+$/)) {
state.registrationForm.valid = true;
state.registrationForm.errors = [];
} else {
state.registrationForm.valid = false;
state.registrationForm.errors.push('wrong format');
}
render(state);
});
Пример того, как render()
может быть устроена внутри:
const render = (state) => {
const submit = document.querySelector(/* .... */);
const input = document.querySelector(/* .... */);
submit.disabled = !state.registrationForm.valid;
if (state.registrationForm.valid) {
input.style.border = null;
} else {
input.style.border = 'thick solid red';
}
};
У такого подхода могут быть проблемы с производительностью из-за необходимости постоянно выполнять поиск элементов по DOM. Чтобы избежать этого, можно превратить представление в объект и сохранить в него все нужные элементы ровно один раз, в момент инициализации.
// реализация c помощью классов:
class View {
constructor(){
this.submit = null;
this.input = null;
}
init() {
this.submit = document.querySelector(/* .... */);
this.input = document.querySelector(/* .... */);
}
render(state) {
this.submit.disabled = !state.registrationForm.valid;
if (state.registrationForm.valid) {
this.input.style.border = null;
} else {
this.input.style.border = 'thick solid red';
}
}
}
const view = new View();
view.init(); // тут делаем выборки
view.render(state);
// реализация с помощью функций:
const init = () => {
const submit = document.querySelector(/* .... */);
const input = document.querySelector(/* .... */);
const state = /* данные состояния */;
// ...
return {
state,
elements: {
input,
submit,
},
};
};
const render = (elements) => { ... };
const elements = init();
render(elements);
С ростом приложения увеличивается и количество обработчиков. Каждый из них может приводить к изменению только части страницы. Как поступать в таком случае? Создавать по рендеру на каждую ситуацию или описывать все возможные изменения в одной функции рендер?
Наиболее простым решением будет привязка таких функций к элементам состояния. Предположим, что у нас есть страница управления списком уроков в курсе. В состоянии это выглядит так:
const state = {
lessons: [/* список уроков */],
// остальная часть состояния
};
Для отрисовки этого списка подойдет одна функция renderLessons()
, которая будет вызываться во всех обработчиках, изменяющих этот список: удаляющих или добавляющих элементы.
// Добавление
el1.addEventListener('submit', (e) => {
// логика
renderLessons(state.lessons);
})
// Удаление
el2.addEventListener('submit', (e) => {
// логика
renderLessons(state.lessons);
})
И самое интересное. Что происходит внутри этой функции? Кажется, что внутри функции рендеринга нужно определять, что произошло, и затем менять необходимую часть DOM, например, удалить какой-то элемент, которого больше нет. В реальности это очень затратный подход, его сложно программировать, так как появляется большое количество условных конструкций. Намного проще выполнять полную перерисовку в любой ситуации. Тогда код останется максимально простым.
У такого подхода есть серьезный недостаток – производительность. Но учитывайте два важных момента. Во-первых, производительность — далеко не всегда проблема. Например, при реализации автокомплитов именно так и нужно поступать. Все будет работать быстро в любом случае. Во-вторых, именно эту проблему решают современные фронтенд-фреймворки. Они сами знают, как эффективнее всего обновить DOM.
Теперь наше приложение разделено на три независимых части: состояние (данные приложения), обработчики и рендеринг. Эта модель работы на тривиальных приложениях (в пару-тройку обработчиков) смотрится избыточной, но если обработчиков станет хотя бы 10, то вы увидите, что с приложением достаточно удобно работать. Виден поток данных, то есть движение данных в приложении от одних частей к другим, от обработчика до отрисовки в DOM. Всегда можно отследить, что изменилось, и как одни части приложения зависят от других. К тому же, сокращается дублирование. Например, изменение состояния может идти из разных частей приложения, но логика отрисовки при этом остается неизменной. В такой ситуации достаточно описать новый способ изменения уже существующего состояния, а рендеринг сделает все остальное.
Кроме наличия разделения на три части, не менее важно то, как они друг с другом взаимодействуют:
- Состояние не знает ничего про остальные части системы — оно ядро
- Рендеринг пользуется состоянием для отрисовки (добавление, изменение или удаление элементов) и добавляет новые обработчики в DOM
- Обработчики знают про состояние, так как обновляют его и инициируют рендеринг
Этот способ разделения по-прежнему обладает одним важным недостатком, который мы устраним в уроке, посвященном MVC.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.