На заре интернета веб-сайт представлял собой набор текстовых страниц, изредка разбавленных изображениями. Фактически, ни о каком превосходстве над книжным форматом речи не шло. Но время не стоит на месте, и сегодняшние веб-сайты стали яркими, интерактивными, живыми. Не последнюю роль в оживлении веб-сайтов занимают DOM-события.

Для начала разберемся с терминологией. DOM (от англ. Document Object Model — «объектная модель документа») — это программный интерфейс, позволяющий разработчикам получить доступ к содержимому HTML, XHTML и XML-документов из среды JavaScript. Каждому HTML-элементу на странице соответствует объект в JavaScript (назовем такие объекты узлами, как в википедии). Узлы можно удалять, добавлять и изменять, тем самым изменяя и сам документ.

У каждого такого узла есть целый набор свойств (разнообразные атрибуты и их значения, узел родительского элемента, узлы дочерних элементов и т.д.) и методов (для изменения свойств, добавления, изменения и удаления узлов дочерних элементов и т.д.). Благодаря всему этому функционалу разработчик получает возможность взаимодействовать со всеми элементами на странице.

Но как сделать страницу «живой»? Как заставить ее реагировать на действия пользователя: нажатия клавиш на клавиатуре и мыши или прокрутку страницы? Именно эту задачу и решают DOM-события. У каждого DOM-узла существует целый набор событий, для всех мыслимых и немыслимых действий пользователя. Их настолько много, что на перечисление и описание каждого из них у меня ушло бы не менее месяца. А за это время шустрые разработчики браузеров добавят еще пачку.

Основы работы с событиями

Чтобы разобраться в принципах работы событий, рассмотрим простой пример. Допустим, у нас на странице есть элемент div, пусть это будет модальное окошко. В этом модальном окошке есть кнопка "Закрыть". Нажатие на эту кнопку должно приводить к скрытию элемента div. Имеем такой фрагмент HTML:

<div id="modal-window"><p>Текст модального окошка, например сообщение об ошибке</p><button id="modal-window-close">Закрыть</button></div>

Скрывать элемент div будем изменением значения CSS-свойства display на none. Для упрощения я воспользуюсь JavaScript библиотекой jQuery. Вот как это делается:

$('#modal-window-close').on('click', function () {
    $('#modal-window').css('display', 'none');
});

В моем примере метод on() позволяет добавить к любому элементу функцию, которая будет вызвана при возникновении какого-либо события. Такая функция называется обработчиком события. Количество обработчиков события, которые можно добавить на событие элемента, не ограничено. Это означает, что можно создать одну кнопку, которая выполняет десятки и сотни операций одновременно. Помимо очевидной причины, а именно бесполезности такой кнопки, так не делают еще и потому, что каждый обработчик события будет находиться в памяти, и слишком большое их количество может заметно повлиять на скорость работы веб-сайта.

Создание типовых обработчиков событий

Вернемся к моему примеру. Сейчас там только одно модальное окошко. Мы легко можем добавить еще парочку, но что если на нашем сайте их десятки или сотни? Очевидным способом решения этой проблемы будет написание кода, который будет находить все типовые элементы на странице — модальные окна, в нашем случае — и добавлять обработчик события к каждому из них. Это не самое лучшее решение, но в целях обучения давайте рассмотрим, как это сделать.

Для начала, нам нужно добавить всем модальным окнам некоторый признак, по которому мы сможем отличать их от других элементов на странице. Мы не можем использовать для этого аттрибут id, ведь он должен быть уникальным, вместо него мы воспользуемся классами. Так будет выглядеть наш HTML:

<div class="modal-window"><p>Текст модального окошка, например сообщение об ошибке</p><button class="close">Закрыть</button></div>

А теперь JavaScript:

$('.modal-window').each(function () {
    var $this = $(this);
    $this.find('.close').on('click', function () {
        $this.css('display', 'none');
    });
});

Метод each() позволяет выполнить определенную функцию для каждого из найденных на странице элементов. При этом this будет указывать на тот элемент, для которого в данный момент выполняется эта функция. Я сразу оборачиваю этот элемент в jQuery объект. Затем, с помощью метода find(), я нахожу все дочерние элементы, у которых установлен класс close, и создаю для них обработчик события click, скрывающий текущее модальное окно.

Я мог бы сразу найти все кнопки закрытия всех модальных окон и установить на них общий обработчик события click, но тогда нажатие на любую из этих кнопок закрывало бы все модальные окна, а не только то, к которому принадлежит нажатая кнопка. Для обхода этого эффекта я могу определять модальное окно после нажатия кнопки:

$('.modal-window .close').on('click', function () {
    $(this).parents('.modal-window').first().css('display', 'none');
});

В этом варианте я использую метод parents() для нахождения всех родительских элементов, соответствующих селектору .modal-window. Поиск родительских элементов осуществляется начиная с элемента, на котором был вызван метод parents(), и поднимается вверх по иерархии элементов вплоть до элемента html. Поэтому, первым элементом в списке найденных всегда будет ближайший родитель. Его я выбираю из списка методом first(). В теории модальные окна не должны быть вложены друг в друга, поэтому вызов метода first() можно было бы убрать.

Делегирование событий

Несмотря на то, что в нашем коде обработчик события click всего один, за кулисами он копируется для каждого найденного элемента. К счастью, у DOM-событий есть одна важная особенность: после того, как были запущены все обработчики события на элементе, на котором событие было создано (в нашем случае — кнопка "Закрыть"), вызываются обработчики одноименного события на родительском элементе, и так до самого элемента html. Этот процесс называется "цепь событий" (англ. Event Bubbling). Сделано это для того, чтобы нажатие на картинку внутри ссылки приводило к срабатыванию события click на самой ссылке.

Но мы с вами можем использовать этот механизм для создания общего обработчика события click для всех кнопок на общем элементе-родителе. Для верности, в качестве общего элемента-родителя будем использовать document:

$(document).on('click', function (event) {
    var $target = $(event.target);
    if ($target.is('.modal-window .close')) {
        $target.parents('.modal-window').first().css('display', 'none');
    }
});

Теперь нажатие на любом элементе на странице приведет к выполнению нашего обработчика. Я не упоминал об этом ранее, но в каждый обработчик передается объект Event, в котором указаны детали события. Для нажатия клавиши на клавиатуре там хранится, например, код клавиши. Ну а для нажатия кнопки мыши там, помимо всего прочего, хранится элемент, на который изначально нажал пользователь. Этот элемент хранится в свойстве target объекта Event.

В моем примере я использую метод is() для проверки, соответствует ли нажатый элемент селектору .modal-window .close, ведь наш обработчик будет вызван при нажатии на абсолютно любой элемент на странице. Если была нажата одна из наших кнопок, соответствующее модальное окно будет закрыто.

Более того, если в процессе работы мы добавим на страницу новое модальное окно, кнопка "Закрыть" в нем заработает автоматически, в то время как в предыдущем примере она не оказалась бы в списке элементов, к которым мы добавляли обработчики события, ведь в момент их добавления нашей новой кнопки еще не было на странице.

Казалось бы, код работает, а жизнь прекрасна, но есть нюанс. Он не возникнет в нашем примере, но для наглядности давайте изменим наш HTML:

<div class="modal-window"><p>Текст модального окошка, например сообщение об ошибке</p><button class="close"><span>Закрыть</span></button></div>

Теперь наш код не работает. Почему? Дело в том, что фактически событие click теперь происходит на элементе span. Цепь событий, как и раньше, проходит через саму кнопку, но это не имеет значения, ведь в нашем обработчике мы проверяем на соответствие селектору именно элемент, который был нажат. Но ничего страшного, ведь разработчики jQuery уже решили эту проблему за нас:

$(document).on('click', '.modal-window .close', function () {
    $(this).parents('.modal-window').first().css('display', 'none');
});

Теперь селектор для проверки мы передаем непосредственно в метод on(). За кулисами jQuery проверяет нажатый элемент и всех его родителей на соответствие селектору, и если хоть один родитель этому селектору соответствует, вызывается наш обработчик события. Более того, jQuery передает ссылку на соответствующий селектору элемент в переменную this, как будто обработчик был установлен именно для него!

Используя эту технику, можно создавать сервисы, повсеместно использующие динамическую подгрузку данных. Например ленту сообщений с кнопками действий для каждого из них (скажем, удаление или перемещение в корзину). Не нужно переживать и мучаться с добавлением обработчиков для каждой новой кнопки, достаточно написать его один раз и он будет работать до тех пор, пока открыта веб-страница.

Разумеется, я описал далеко не все возможности DOM-событий. В этой статье я постарался максимально наглядно продемонстрировать конкретную проблему и наиболее удачный, на мой взгляд, способ ее решения. Возможно, я вернусь в теме DOM-событий в будущем, а на сегодня все.