Классы и прототипы
Функции модели уже отделены от страницы, а состояние взаимодействия стало достаточно заметным, чтобы дать ему собственный объект. Класс CatalogController объединит ссылки на узлы, текущие курсы и операции подключения. Через него рассмотрим экземпляры и прототипы без превращения всех простых вычислений в методы.
Снимок advanced/lesson-35 является новым вариантом локального каталога, не продолжением постраничного запроса на той же странице. Четыре курса находятся в общем модуле данных. Первоначальный catalog-lab не меняется. Код при подготовке не запускался, поэтому количество карточек и вывод о прототипе остаются ожидаемыми результатами.
Собственные поля экземпляра
Конструктор получает контейнер и массив:
export class CatalogController {
constructor(host, courses = []) {
this.host = host;
this.courses = validateCourses(courses);
this.selection = { topic: 'all', sort: 'original' };
this.lifetime = null;
this.nodes = null;
}
}
Этот фрагмент показывает начало полного catalog-controller.js, а не готовую замену всего файла. Модуль импортирует проверку модели и функцию представления. После конструктора находятся методы, перечисленные далее. Используйте полный снимок или перенесите их согласованно, не создавая второе объявление класса.
Вызов new создаёт экземпляр, а конструктор записывает его собственные поля. Два контроллера могут иметь разные контейнеры и выбор, хотя созданы одним классом. Данные проверяются на входе, поэтому последующие операции модели получают известный формат.
Класс не является отдельным форматом JSON. Его экземпляр содержит функции поведения через прототип, ссылки на DOM и состояние жизненного цикла. Сохранять такой объект как настройки из урока 27 бессмысленно: постоянное представление должно содержать только данные собственного договора.
Методы и прототип
Методы, объявленные в теле класса, находятся в его прототипе, а не копируются текстом в каждый экземпляр. Когда читается catalog.render, поиск находит соответствующую функцию через цепочку прототипов. Получатель вызова всё равно остаётся экземпляром catalog.
const catalog = new CatalogController(host, courses);
console.log(Object.getPrototypeOf(catalog) === CatalogController.prototype);
Ожидается true. Это наблюдение устройства экземпляра, а не проверка качества данных. Два объекта могут иметь один прототип и разное содержимое полей. instanceof также не заменяет validateCourses: наследование и допустимость внешнего массива отвечают на разные вопросы.
Основные правила классов и их связи с прототипами представлены в MDN о classes. В этой серии не вводим иерархию наследования только ради термина: одному локальному контроллеру достаточно конкретного класса с явными обязанностями.
Подключение поведения
Метод mount сначала проверяет, не подключён ли контроллер уже. Узлы создаются один раз, затем создаётся отдельный сигнал слушателей:
mount() {
if (this.lifetime) return;
if (!this.nodes) this.createNodes();
this.lifetime = new AbortController();
this.nodes.form.addEventListener('submit', event => {
event.preventDefault();
this.setSelection({
topic: this.nodes.select.value,
sort: this.selection.sort,
});
}, { signal: this.lifetime.signal });
this.render();
}
Это метод полного класса. Стрелочный callback использует this из вызова mount, что сохраняет нужный экземпляр согласно уроку 33. Он не полагается на специальный получатель, который браузер мог бы дать обычному обработчику формы.
createNodes создаёт форму, поле темы, кнопку, сообщение, список и небольшой пустой sentinel после списка. Последний пока не имеет наблюдателя; он будет точкой расширения в следующих уроках. Все значения курса вставляются через известное renderCourses, а оболочка создаётся через DOM без внешнего HTML.
Повторный mount не создаёт второй набор формы и слушателей. Это важный договор жизненного цикла, а не просто удобная проверка. Если компонент будет временно отключён и возвращён, он должен сохранять понятное количество узлов и операций.
Отключение и сохранённые узлы
Метод unmount отменяет сигнал и сбрасывает его поле. Это удаляет связанные слушатели, но намеренно не уничтожает DOM:
unmount() {
this.lifetime?.abort();
this.lifetime = null;
}
Отключение слушателя не делает оставшуюся форму недоступной: браузер по-прежнему способен выполнить её обычную отправку с навигацией. Если владелец оставляет отключённый каталог на экране, он отдельно блокирует управление или заменяет его статичным представлением. При временном удалении узла в следующем уроке этот вопрос решается самим отсутствием формы в документе.
При новом mount используется прежняя оболочка, создаётся новый сигнал и подключается ровно один обработчик. Отменённый сигнал нельзя использовать повторно, поэтому создаётся новый контроллер жизненного цикла. Он отличается от контроллера сетевого запроса 22: механизм одинаков, обязанность здесь — слушатели компонента.
Сохранение узлов полезно для повторного подключения. Если продукту нужна окончательная очистка контейнера, следует добавить отдельную операцию с понятным названием. Не объявляйте unmount одновременно удалением данных, DOM и всех внешних ресурсов без такого кода.
Публичные операции
setCourses принимает новый массив, проверяет его и обновляет представление. setSelection разрешает только известные темы и способы порядка. render вычисляет видимый набор из текущего состояния, создаёт карточки и сообщение количества. Ни один метод не читает модель обратно из текста DOM.
setStatus позволяет внешнему управляющему коду установить короткую текстовую подпись. Getters listElement, statusElement, sentinelElement возвращают постоянные узлы после создания. До подключения они могут быть null. Это явный интерфейс расширения, который пригодится наблюдателям, но внешняя сторона должна учитывать момент жизни компонента.
Название свойства nodes само по себе не делает его приватным. В этом учебном варианте поля остаются доступными по правилам языка; сторонний код договорно пользуется публичными операциями. Если понадобится строгая языковая приватность, можно изучить отдельные private-поля, не меняя случайно все потребляющие части.
Полный app.js создаёт один экземпляр на #catalog-host, вызывает mount и выводит длину списка. Ожидаются четыре карточки. При выборе публикации через форму — одна и двенадцать уроков. Простые selectCourses и countLessons остаются чистыми функциями модели, класс только связывает их с жизнью интерфейса.
Два экземпляра и один договор методов
Создание второго контроллера не означает копирование текущего выбора первого. Каждый конструктор получает собственный контейнер, массив курсов и начальную пару настроек. Методы доступны через общий прототип, но читают поля своего получателя. Поэтому публикация в первом каталоге не должна менять тему второго, если внешняя программа не передала им специально общее управление.
При этом объекты внешнего массива тоже требуют понимания ссылок. validateCourses формирует проверенные записи своего договора, а контроллер использует их как состояние. Не редактируйте его поля обходным путём лишь потому, что они доступны технически. Публичный setCourses повторяет проверку и обновляет представление; прямое присваивание может оставить карточки и модель в разных состояниях.
Проверка повторного mount особенно полезна после появления автоматического жизненного цикла. Метод должен безопасно принимать повторное подключение без второго слушателя. Во время unmount форма остаётся видимой, но её обработчик снят. После нового mount новая отправка выполняет одно применение выбора. Этот сценарий показывает конкретное поведение, которое позже браузерные callbacks будут вызывать за нас.
render не является универсальным восстановлением любого случайно испорченного DOM. Контроллер хранит ссылки на созданные узлы и предполагает, что внешняя сторона не заменила их полностью. Если другой код удалил форму и подставил новый контейнер, понадобится новая граница владения. Публичные getters предназначены для аккуратного расширения, а не для произвольного разрушения оболочки.
Sentinel уже создан как точка будущего наблюдения, однако класс пока не хранит IntersectionObserver, таймер или Worker. Поэтому unmount освобождает только реально подключённый ресурс слушателей. Каждое расширение далее должно добавить собственную очистку. Такой пошаговый договор легче читать, чем класс с множеством неиспользуемых полей и обещаний, которые не отражены в реализации.
Для ручного опыта сравните повторный mount, unmount и последующее подключение. Ожидается отсутствие дубликатов и снятие нашего обработчика во время отключения. Проверяйте повторную отправку после нового mount; обычное HTML-действие оставленной формы не является действием контроллера. Затем создайте второй независимый контейнер, если хотите увидеть разные экземпляры. Следующий урок обернёт контроллер в автономный пользовательский HTML-элемент с браузерными callbacks подключения.