Базово слушатель событий работает с событиями, запускаемыми одним элементом:
Однако по мере создания более сложных программ отображение «одного обработчика событий для одного элемента» уже не подойдет. Причина в динамическом создании элементов посредством JavaScript. Эти элементы могут запускать события, которые вам может понадобиться прослушать и на которые соответственно среагировать. При этом вам может потребоваться обработка событий как для нескольких элементов, так и для их множества.
Вряд ли вы захотите делать так:
Вам не захочется создавать слушателя событий для каждого элемента, если слушатель событий для всех них одинаков. Причина в том, что это непродуктивно. Каждый из этих элементов несет в себе данные об одном и том же слушателе событий и его свойствах, что может существенно увеличить потребление памяти при добавлении большого количества содержимого. Вам, наоборот, нужен чистый и быстрый способ обработки событий для множества элементов с минимальным количеством повторений и ненужных компонентов. Предпочтительный вариант в этом случае будет выглядеть примерно так:
Все это может звучать несколько нереально, не так ли? Что ж, в этой главе вы узнаете, что это вполне нормально, и научитесь реализовывать подобное, используя всего несколько строк кода JavaScript.
Поехали!
Как все это делается?
Суть в следующем. Вы знаете, как работает простая обработка событий, когда у вас есть один элемент, один слушатель событий и один обработчик событий. Несмотря на то что случай обработки нескольких элементов может казаться иным, воспользовавшись разрывностью событий, разрешить эту проблему достаточно просто.
Представьте, что есть случай, в котором вы хотите прослушивать событие клика в любом из элементов-братьев со значениями idone, two, three, four и five. Давайте дорисуем картину, изобразив DOM следующим образом:
В самом низу расположены элементы, в которых мы хотим прослушивать события. Все они имеют одного родителя в виде элемента с id, равным theDude. Чтобы разрешить проблему обработки этих событий, давайте рассмотрим сначала плохое решение, а затем его удачную альтернативу.
Плохое решение
Так делать не нужно. Мы не хотим создавать слушателя событий для каждой из кнопок:
let oneElement = document.querySelector("#one");
let twoElement = document.querySelector("#two");
let threeElement = document.querySelector("#three");
let fourElement = document.querySelector("#four");
let fiveElement = document.querySelector("#five");
oneElement.addEventListener("click", doSomething, false);
twoElement.addEventListener("click", doSomething, false);
threeElement.addEventListener("click", doSomething, false);
fourElement.addEventListener("click", doSomething, false);
fiveElement.addEventListener("click", doSomething, false);
function doSomething(e) {
let clickedItem = e.target.id;
console.log("Hello " + clickedItem);
}
Очевидная причина так не делать — в нежелании повторять код. Другая причина состоит в том, что для каждого элемента теперь установлено свойство addEventListener. В случае с пятью элементами это не так страшно. Однако все становится куда серьезнее, когда вы работаете с десятками или сотнями элементов, каждый из которых задействует частичку памяти. Еще одна причина в том, что число элементов может варьировать в зависимости от степени адаптивности или динамичности UI. Ваше приложение может добавлять или удалять элементы в зависимости от действий пользователя, что затруднит отслеживание всех индивидуальных слушателей событий, которые могут потребоваться объекту. Наличие же одного всеобщего обработчика событий существенно упрощает весь этот процесс.
Хорошее решение
Хорошее решение вторит схеме, приведенной ранее, где мы используем всего один слушатель событий. Сначала я вас немного запутаю описанием того, как это работает, а затем попытаюсь распутать, приведя пример кода и подробно пояснив все происходящее. Простое и запутывающее решение:
1. Создать один слушатель событий в родительском элементе theDude.
2. Когда произойдет щелчок по любому из элементов one, two, three, four или five, опереться на поведение распространения, присущее событиям, и прерывать их, когда они достигают элемента theDude.
3. (По желанию) Остановить распространение события в родительском элементе, чтобы оно не отвлекало нас своей безудержной беготней по дереву DOM вверх и вниз.
Не знаю, как вы, но я после прочтения этих пунктов точно запутался. Давайте начнем распутываться, обратившись для начала к схеме, более наглядно представляющей описанные действия:
Последним этапом нашего квеста по распутыванию будет код, подробно расписывающий содержимое схемы и все три шага:
let theParent = document.querySelector("#theDude");
theParent.addEventListener("click", doSomething, false);
function doSomething(e) {
if (e.target!= e.currentTarget) {
let clickedItem = e.target.id;
console.log("Hello " + clickedItem);
}
e. stopPropagation();
}
Уделите время и внимательно прочитайте и проанализируйте этот код. Приняв во внимание наши изначальные цели и схему, мы будем слушать событие в родительском элементе theDude:
let theParent = document.querySelector("#theDude");
theParent.addEventListener("click", doSomething, false);
Обработкой этого события занимается один обработчик, которым является функция doSomething:
function doSomething(e) {
if (e.target!= e.currentTarget) {
let clickedItem = e.target.id;
console.log("Hello " + clickedItem);
}
e. stopPropagation();
}
Этот слушатель событий будет вызван каждый раз, когда будет происходить щелчок как в самом элементе theDude, так и в любом из его потомков. Нас же интересуют только события щелчка потомков. Правильным способом игнорировать щелчки по родительскому элементу будет просто избежать выполнения любого кода, если элемент, на котором произошел щелчок (то есть целевое событие), совпадает со слушателем событий (то есть элементом theDude):
function doSomething(e) {
if (e.target!= e.currentTarget) {
let clickedItem = e.target.id;
console.log("Hello " + clickedItem);
}
e. stopPropagation();
}
Цель события представлена e.target, а целевой элемент, к которому прикреплен слушатель событий, — e.currentTarget. Простая проверка равенства этих событий даст гарантию, что обработчик событий не среагирует на ненужные вам события, запущенные из родительского элемента.
Чтобы остановить распространение события, мы просто вызываем метод stopPropagation:
function doSomething(e) {
if (e.target!= e.currentTarget) {
let clickedItem = e.target.id;
console.log("Hello " + clickedItem);
}
e. stopPropagation();
}
Обратите внимание, что этот код располагается вне инструкции if. Я сделал так, чтобы остановить перемещение события по DOM во всех случаях, как только оно будет услышано.
Объединяя все сказанное
В результате выполнения всего этого кода вы можете щелкнуть по любому потомку theDude и прослушать событие при его распространении вверх:
Поскольку все аргументы событий по-прежнему уникальны для элемента, с которым мы взаимодействуем (то есть источника события), мы также можем распознать и выделить нажатый элемент изнутри обработчика событий, несмотря на то что addEventListener активна только в родителе. Главное в этом подходе то, что он решает обозначенные проблемы. Вы создали всего один обработчик событий, и не важно, сколько в итоге будет потомков у theDude. Этот подход достаточно универсален и способен справиться со всеми ими, не требуя для этого дополнительного изменения кода. Это также значит, что понадобится произвести строгую фильтрацию, если потомками элемента theDude в итоге будут не только кнопки, но и другие важные для вас элементы.
КОРОТКО О ГЛАВНОМ
Некоторое время назад я предложил решение загадки этого многоэлементного троеборья (крутые ребята говорят: MEEC), которое оказалось непродуктивным, но при этом не требовало повторения множества строк кода. До тех пор пока многие разработчики не указали мне на его непродуктивность, я считал его вполне рабочим.
В этом решении использовался цикл for для прикрепления слушателей событий ко всем потомкам родителя (или массива, содержащего HTML-элементы). Вот как выглядел его код:
let theParent = document.querySelector("#theDude");
for (let i = 0; i < theParent.children.length; i++) {
let childElement = theParent.children[i];
childElement.addEventListener('click', doSomething, false);
}
function doSomething(e) {
let clickedItem = e.target.id;
console.log("Hello " + clickedItem);
}
В итоге этот подход позволял прослушивать события щелчка непосредственно в потомках. Единственным, что мне пришлось прописывать вручную, был один вызов слушателя событий, который параметризировался для соответствующего дочернего элемента в зависимости от того, где в цикле находился код:
childElement.addEventListener('click', doSomething, false);
Причина несостоятельности этого подхода в том, что каждый дочерний элемент имеет связанный с ним слушатель событий. Это возвращает нас к вопросу об эффективности, которая в данном случае страдает от неоправданных затрат памяти.
Если у вас возникнет ситуация, в которой элементы будут разбросаны по DOM, не имея рядом общего родителя, использование этого подхода для массива HTML-элементов будет неплохим способом решения проблемы MEEC.
Как бы то ни было, когда вы начинаете работать с большим количеством элементов UI в играх, приложениях для визуализации данных и прочих насыщенных элементами программах, то будете вынуждены использовать все описанное в этой главе по меньшей мере один раз. Надеюсь, что если все остальное не сработает, то эта глава послужит на славу. При этом весь материал, касающийся перемещения и перехвата событий, рассмотренный ранее, здесь оказался весьма кстати.