DOM и события
Что это такое
DOM — живая объектная модель документа в браузере: по одному узлу на элемент, атрибут и фрагмент текста, доступная из JavaScript и изменяемая на месте. События — сообщения, которые браузер посылает, когда что-то происходит: клик, нажатие клавиши, ответ сети. Слушают их именно в DOM.
Почему это важно
Здесь страница перестаёт быть документом и становится приложением. Любое интерактивное поведение — от выпадающего списка до живого поиска — складывается из работы с DOM и обработки событий. Здесь же живут две самые частые проблемы производительности и корректности: слушатели, которые продолжают работать после исчезновения элемента, и обработчики, которые обновляют DOM так, что браузер пересчитывает раскладку снова и снова. От кода, который отвечает мгновенно, код, который заикается, отличает понимание API — и его цены.
Ключевые идеи
Сначала запросите дерево, потом меняйте его
querySelector и querySelectorAll принимают CSS-селекторы, поэтому знание селекторов из CSS применимо напрямую. Меняйте элементы, задавая свойства (textContent, classList, value), а не переписывая HTML-строки: innerHTML разбирает и пересобирает всё заново, а это медленнее и открывает путь для внедрённой разметки.
const list = document.querySelector("#results");
list.textContent = "";
Осторожно с этим: querySelectorAll возвращает статический NodeList, а не живой, поэтому обходите его через for...of или Array.from, а не методами массива.
Стройте узлы вместо склейки строк
document.createElement и append дают настоящий узел, свойства которого можно задавать, а раскладку браузер пересчитывает один раз — при вставке. Если же собрать строку и присвоить её, всё разберётся заново, а старое поддерево будет выброшено.
const item = document.createElement("li");
item.textContent = row.name;
list.append(item);
События всплывают, и это возможность
Событие срабатывает на своей цели, а затем идёт вверх по предкам. Слушатель на контейнере, который проверяет event.target, называют делегированием: он один покрывает все строки, включая добавленные позже. Так лечится и проблема «мой слушатель пропал», и проблема «у меня двести слушателей».
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-id]");
if (!button) return;
removeRow(button.dataset.id);
});
Предотвращайте стандартное поведение, только когда это осознанно
event.preventDefault() останавливает собственное поведение браузера: переход по ссылке, отправку формы. Вызов его на каждом клике, чтобы «страница не прыгала», ломает средний клик, активацию с клавиатуры и валидацию формы. stopPropagation нужен для вложенных интерактивных областей, и прибегать к нему стоит редко.
Убирайте то, что навесили
Слушатель держит свой элемент и своё замыкание живыми, пока элемент прикреплён. Удаляете элемент — его слушатели уходят вместе с ним; однако если они навешены на window или document, этого не произойдёт. Снимайте всё долгоживущее.
Практика
Отрисуйте список через делегирование
Постройте список: элементы берутся из массива данных, в каждой строке есть кнопка удаления, а слушатель клика на контейнере один.
Готово, когда: клик по кнопке любой строки удаляет эту строку, а строка, добавленная после загрузки страницы, работает без нового слушателя.
Посмотрите на reflow в панели производительности
Напишите цикл, который на каждой итерации читает offsetHeight, а затем пишет стиль. Запишите профиль, после чего перестройте цикл так, чтобы сначала шло чтение, а запись — потом, и запишите снова.
Готово, когда: вторая запись показывает, что работа раскладки сгруппирована, а не повторяется на каждой итерации.
Частые ошибки
- Вызов
querySelectorAllи последующее использование методов массива — методаmapуNodeListнет, и код падает; исправить: преобразуйте черезArray.fromили обходите черезfor...of. - Навешивание слушателя внутри цикла по тем же элементам — работа растёт вместе со списком, а обработчики дублируются и срабатывают вместе; исправить: навесьте один делегированный слушатель на контейнер.
- Чтение свойств раскладки после каждой записи — каждое чтение заставляет браузер пересчитывать раскладку, и цикл заикается; исправить: группируйте чтения, затем записи.
- Использование
innerHTMLс пользовательским содержимым — браузер разберёт строку как разметку, и недоверенный текст превратится в элемент; исправить: задавайтеtextContentили стройте узлы. - Забыть, что
eventисчезает после возврата обработчика — он переиспользуется, а его свойства очищаются; исправить: читайте нужное в переменную прямо во время работы обработчика.
Что читать дальше
- Синтаксис и семантика (02-frontend/06-javascript/01-syntax-and-semantics) — что увидят обработчики в момент срабатывания, решают замыкания и
this. - Асинхронный JavaScript (02-frontend/06-javascript/03-asynchronous-javascript) — почти каждый настоящий обработчик запускает работу, которая завершится позже.
- Инструменты разработчика в браузере (02-frontend/27-browser-devtools) — в панели Elements видно живое дерево и слушателей событий на каждом узле.