Чтобы страница реагировала на действия посетителя, нужен JavaScript. Он находит элементы, меняет их содержимое, показывает и скрывает блоки, отправляет формы без перезагрузки. В основе всего лежат две вещи: модель документа, которую браузер строит из вашей разметки, и система событий, через которую пользователь «разговаривает» со страницей.
В статье разберём обе темы на практике: как работать с деревом элементов, как подписываться на события, чем отличается всплытие от погружения и зачем нужно делегирование. Примеры написаны на чистом JavaScript, без библиотек.
Что такое DOM
Когда браузер загружает страницу, он разбирает HTML и строит из него дерево объектов. Каждый тег, каждый фрагмент текста становится узлом. Это дерево и называется DOM — объектная модель документа. JavaScript работает именно с ней, а не с исходным текстом разметки.
Важные следствия:
- изменение узла в скрипте сразу меняет отображение страницы;
- DOM существует только пока открыта страница, исходный файл при этом не меняется;
- у каждого элемента есть родитель, дети и соседи, и по этим связям можно перемещаться;
- элементы имеют свойства и методы: содержимое, атрибуты, стили, список классов.
Поиск элементов
Прежде чем что-то менять, элемент нужно найти. Современный и универсальный способ — запрос по селектору, такому же, как в стилях.
const title = document.querySelector('h1');
const items = document.querySelectorAll('.menu li');
const form = document.getElementById('order-form');
Первый метод возвращает первый подходящий элемент или пустое значение, если ничего не найдено. Второй возвращает набор всех подходящих, по которому можно пройтись циклом. Третий ищет по идентификатору и работает быстрее остальных, но подходит только для уникальных идентификаторов.
Всегда проверяйте, что элемент найден, прежде чем обращаться к его свойствам. Самая частая ошибка новичков — попытка работать с пустым значением, после чего в консоли появляется сообщение о чтении свойства у несуществующего объекта.
Чтение и изменение содержимого
У элементов есть несколько способов работы с содержимым, и между ними важная разница.
- textContent читает и записывает только текст. Теги в нём не интерпретируются, поэтому способ безопасен.
- innerHTML работает с разметкой. Удобно, но опасно: если вставить в него данные пользователя, злоумышленник может внедрить свой скрипт.
- value используется для полей ввода.
- setAttribute и getAttribute управляют атрибутами.
const msg = document.querySelector('#message');
msg.textContent = 'Заявка отправлена';
msg.classList.add('success');
msg.classList.remove('error');
Для стилей лучше менять классы, а не писать правила напрямую в элемент. Так внешний вид остаётся в таблице стилей, а скрипт отвечает лишь за переключение состояний. Метод переключения класса удобен для меню и раскрывающихся блоков.
Создание и удаление элементов
Новые элементы создаются методом создания, затем настраиваются и добавляются в нужное место дерева.
const li = document.createElement('li');
li.textContent = 'Новый пункт';
document.querySelector('#list').appendChild(li);
Для удаления у элемента есть собственный метод, который убирает его из дерева. Если вы добавляете много элементов подряд, собирайте их в промежуточный контейнер-фрагмент и вставляйте одним действием. Это уменьшает количество перерисовок и ускоряет работу.
События: основы
Событие — это сигнал о том, что что-то произошло: пользователь кликнул, нажал клавишу, изменил поле, закончилась загрузка страницы. Чтобы реагировать, на элемент вешают обработчик — функцию, которую браузер вызовет при наступлении события.
const button = document.querySelector('#send');
button.addEventListener('click', function (event) {
console.log('Нажата кнопка');
});
Рекомендуемый способ подписки — метод добавления слушателя. Он позволяет навесить несколько обработчиков на одно событие, не затирая друг друга, и аккуратно снимать их при необходимости. Старый способ через свойство вроде onclick хранит единственный обработчик, и повторное присвоение его заменяет.
Самые востребованные события:
- click, dblclick: клики мышью;
- input, change: изменение значения в поле;
- submit: отправка формы;
- keydown, keyup: нажатие клавиш;
- mouseover, mouseout: наведение курсора;
- scroll, resize: прокрутка и изменение размера окна;
- DOMContentLoaded, load: готовность документа и полная загрузка ресурсов.
Объект события
Обработчик получает объект с описанием события. Из него можно узнать, на каком элементе всё произошло, какая клавиша нажата, где находился курсор.
- event.target — элемент, на котором событие возникло на самом деле.
- event.currentTarget — элемент, на котором висит обработчик.
- event.preventDefault() отменяет стандартное поведение: переход по ссылке, отправку формы.
- event.stopPropagation() прекращает дальнейшее распространение события.
- event.key содержит название нажатой клавиши.
form.addEventListener('submit', function (event) {
event.preventDefault();
if (!form.elements.email.value) {
alert('Укажите адрес почты');
}
});
Отмену стандартного поведения используют, когда нужно проверить форму или отправить данные через фоновый запрос, не перезагружая страницу.
Всплытие и погружение
Событие не ограничивается элементом, на котором оно произошло. Сначала оно проходит вниз от корня документа к цели (фаза погружения), затем поднимается обратно вверх (фаза всплытия). По умолчанию обработчики срабатывают на всплытии.
Пример: в списке лежит кнопка. Клик по ней вызовет обработчик на кнопке, затем на элементе списка, затем на контейнере и так до самого верха. Этим можно пользоваться, но нужно учитывать: если на родителе висит свой обработчик клика, он сработает при клике по любому дочернему элементу.
Остановка всплытия кажется простым решением, но злоупотреблять ей не стоит: она ломает чужие обработчики, например компоненты аналитики, которые ждут события на верхнем уровне. Применяйте её точечно и осознанно.
Делегирование событий
Представьте таблицу на сотню строк с кнопкой удаления в каждой. Навешивать сотню обработчиков неудобно и затратно. К тому же новые строки, добавленные скриптом позже, останутся без обработчиков.
Решение — делегирование: один обработчик на общем родителе, который с помощью объекта события определяет, по какому именно элементу кликнули.
document.querySelector('#table').addEventListener('click', function (event) {
const btn = event.target.closest('.delete');
if (!btn) return;
const row = btn.closest('tr');
row.remove();
});
Метод поиска ближайшего предка с нужным селектором делает код устойчивым, даже если внутри кнопки лежат иконки или вложенные теги. Делегирование экономит память и автоматически работает для элементов, которые появятся позже.
Когда запускать скрипт
Если скрипт подключён в начале страницы и обращается к элементам, которых браузер ещё не построил, поиск вернёт пустое значение. Есть несколько способов избежать этого.
- Подключайте скрипт в конце тела документа.
- Добавляйте атрибут отложенной загрузки, чтобы скрипт выполнялся после разбора разметки.
- Оборачивайте код в обработчик события готовности документа.
Второй вариант сегодня предпочтителен: страница не блокируется загрузкой, а скрипт гарантированно видит готовое дерево.
Типичные ошибки начинающих
- Обращение к элементу до того, как он создан.
- Использование innerHTML с пользовательскими данными.
- Подписка на события в цикле без делегирования, особенно на динамических списках.
- Забытое снятие обработчиков при удалении элементов, из-за чего растёт потребление памяти.
- Путаница между event.target и event.currentTarget.
- Бесконтрольная остановка всплытия.
- Частые обращения к DOM в цикле: чтение и запись размеров вперемешку вызывают лишние пересчёты вёрстки.
- Обработчики прокрутки и изменения размера без ограничения частоты вызовов.
Для последнего случая применяют приём, при котором функция вызывается не чаще заданного интервала. Это заметно разгружает браузер на слабых устройствах.
Влияние на SEO и доступность
Вебмастеру важно помнить: контент, который появляется только после действий пользователя или загружается скриптом, может быть проиндексирован хуже. Важный текст держите в исходной разметке, а скрипты используйте для улучшения интерфейса. Меню, ссылки и основные тексты должны работать и без сложных сценариев.
Не забывайте о клавиатуре и вспомогательных технологиях: если вы делаете кликабельным не кнопку, а обычный блок, добавьте поддержку клавиш и понятные атрибуты. Лучше используйте настоящие кнопки и ссылки: они из коробки поддерживают фокус и клавиатуру.
Заключение
DOM и события — фундамент клиентского JavaScript. Научившись находить элементы, менять их содержимое и классы, подписываться на события и использовать делегирование, вы сможете написать большинство интерактивных элементов сайта без громоздких библиотек: вкладки, раскрывающиеся списки, проверку форм, динамические списки.
Практический совет: возьмите небольшую задачу, например список дел с добавлением и удалением пунктов, и реализуйте её целиком на чистом JavaScript. Эта простая работа затрагивает поиск элементов, создание узлов, события и делегирование, то есть всё, что мы разобрали. После неё библиотеки и фреймворки станут понятнее, потому что вы будете знать, что они делают под капотом.