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 видно живое дерево и слушателей событий на каждом узле.