Сейчас мы с вами разберем некоторые продвинутые вещи при работе с объектом Event, а именно: всплытие и перехват, а также делегирование событий.
Всплытие событий
Представьте себе, что у вас есть несколько вложенных друг в друга блоков:
<div>
<div>
<div>самый внутренний блок</div>
</div>
</div>
Когда вы кликаете на самый внутренний блок, событие onclick возникает сначала в нем, а затем срабатывает в его родителе, в родителе его родителя и так далее, пока не дойдет то тега body и далее до тега html (затем до document и до window).
И это логично, ведь кликая на внутренний блок, вы одновременно кликаете на все внешние.
Давайте убедимся в этом на следующем примере: у нас есть 3 блока, к каждому из них привязано событие onclick:
<div onclick="alert('зеленый');">
<div onclick="alert('голубой');">
<div onclick="alert('красный');"></div>
</div>
</div>
Нажмите на самый внутренний красный блок - и вы увидите, как сначала сработает onclick красного блока, потом голубого, потом зеленого:
Такое поведение называется всплытием событий - по аналогии со всплытием пузырька воздуха со дна. Так же, как и пузырек, наш клик по внутреннему элементу как будто выплывает наверх, каждый раз срабатывая на более высоких блоках.
event.target
Пусть у нас есть два элемента: div и абзац p, лежащий внутри этого дива. Пусть onlick мы привязали в диву:
<div onclick="alert('!');">
<p></p>
</div>
Когда мы кликаем на этот див, мы можем попасть по абзацу, а можем попасть в место, где этого абзаца нет.
Как такое может быть - посмотрите на следующем примере: зеленый цвет - это наш див, а голубой - наш абзац:
Если кликнуть в зеленую часть - мы кликнем именно по диву, а если кликнуть на голубую часть - клик произойдет сначала по абзацу, а потом уже по диву. Но так как onclick привязан именно к диву - мы в общем-то присутствие абзаца можем и не заметить.
Однако, иногда нам хотелось бы знать - клик произошел непосредственно по диву или по его потомку абзацу. В этом нам поможет объект Event и его свойство event.target - в нем хранится именно тот элемент, в котором произошел клик.
В следующем примере у нас есть div, внутри него лежит p, а внутри него - span.
Давайте привяжем событие onclick самому верхнему элементу (диву) и будем кликать на разные элементы: на div, на p, на span. С помощью event.target получим самый нижний элемент, в котором случилось событие и выведем его название с помощью tagName.
Если кликнуть, к примеру, на span - то событие отловит наш div (ведь именно к нему привязан onclick), но в event.target будет лежать именно span:
<div onclick="alert(event.target.tagName);">
<p>
<span></span>
</p>
</div>
Покликайте по разным блокам - вы увидите результат:
Прекращение всплытия
Итак, вы уже знаете, что все события всплывают до самого верха (до тега html, а затем до document, а затем до window). Иногда есть нужда это всплытие остановить. Это может сделать любой элемент, через который всплывает событие. Для этого в коде элемента следует вызвать метод event.stopPropagation().
В следующем примере клик по красному блоку сработает на нем самом, затем на голубом блоке и все - голубой блок прекращает дальнейшее всплытие и зеленый блок уже никак не отреагирует:
<div onclick="alert('зеленый');">
<div onclick="alert('голубой'); event.stopPropagation();">
<div onclick="alert('красный');"></div>
</div>
</div>
Кликните на красный блок - вы увидите результат:
Погружение
Кроме всплытия событий есть еще и погружение (по научному стадия перехвата). Это значит, что событие сначала идет сверху вниз (стадия перехвата), доходит до нашего элемента (стадия цели) и только потом начинает всплывать (стадия всплытия).
Повесить обработчик события с учетом стадии перехвата можно только с помощью addEventListener. Для этого у него есть третий параметр: если он равен true - событие сработает на стадии перехвата, а если false - на стадии всплытия (это по умолчанию):
var green = document.getElementById('green');
green.addEventListener('click', func, true);
function func(event) {
}
Стадию, на которой произошло событие можно определить с помощью свойства event.eventPhase. Оно может принимать следующие значения: 1 - стадия перехвата, 2 - стадия цели, 3 - стадия всплытия.
Вступление к делегированию
Представим себе ситуацию: пусть у нас есть ul с несколькими li. К каждой li привязано следующее событие: по нажатию на li ей в конец добавляется '!'.
Давайте реализуем описанное:
<ul id="ul">
<li>пункт 1</li>
<li>пункт 2</li>
<li>пункт 3</li>
<li>пункт 4</li>
<li>пункт 5</li>
</ul>
var li = document.querySelectorAll('#ul li');
//В цикле вешаем функцию addSign на каждую li:
for (var i = 0; i < li.length; i++) {
li[i].addEventListener('click', addSign);
}
function addSign() {
this.innerHTML = this.innerHTML + '!';
}
Понажимайте на li - вы увидите, как им в конец добавляется '!':
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Пусть теперь у нас также есть кнопочка, по нажатию на которую в конец ul добавляется новая li с текстом 'пункт'. Нас ждет сюрприз: привязанное событие не будет работать для новых li! Убедимся в этом:
<ul id="ul">
<li>пункт 1</li>
<li>пункт 2</li>
<li>пункт 3</li>
<li>пункт 4</li>
<li>пункт 5</li>
</ul>
<button id="button">Добавить li</button>
var li = document.querySelectorAll('#ul li');
for (var i = 0; i < li.length; i++) {
li[i].addEventListener('click', addSign);
}
function addSign() {
this.innerHTML = this.innerHTML + '!';
}
//Реализация кнопочки добавления новой li:
var ul = document.getElementById('ul');
var button = document.getElementById('button');
button.addEventListener('click', addLi);
function addLi() {
var li = document.createElement('li');
li.innerHTML = 'новая li';
ul.appendChild(li);
}
Нажмите на кнопочку для добавления li, а затем на эту новую li - она не среагирует:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Для решения проблемы можно в момент создания новой li повесить на нее функцию addSign через addEventListener. Давайте реализуем это:
<ul id="ul">
<li>пункт 1</li>
<li>пункт 2</li>
<li>пункт 3</li>
<li>пункт 4</li>
<li>пункт 5</li>
</ul>
<button id="button">Добавить li</button>
var li = document.querySelectorAll('#ul li');
for (var i = 0; i < li.length; i++) {
li[i].addEventListener('click', addSign);
}
function addSign() {
this.innerHTML = this.innerHTML + '!';
}
//Реализация кнопочки добавления новой li:
var ul = document.getElementById('ul');
var button = document.getElementById('button');
button.addEventListener('click', addLi);
function addLi() {
var li = document.createElement('li');
li.innerHTML = 'новая li';
li.addEventListener('click', addSign); //навесим событие на новую li
ul.appendChild(li);
}
Нажмите на кнопочку для добавления li, а затем на эту новую li - она среагирует:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Существует и второй способ обойти проблему - делегирование событий. Давайте его разберем.
Делегирование событий
Суть делегирования в следующем: навесим событие не на каждую li, а на их родителя - на ul.
При этом работоспособность нашего скрипта должна сохраниться: по-прежнему при клике на li ей в конец будет добавляться '!'. Только событие в новом варианте будет навешано на ul:
var ul = document.getElementById('ul');
//Вешаем событие на ul:
ul.addEventListener('click', addSign);
function addSign() {
}
Как мы это провернем: так как событие навешано на ul, то внутри функции мы можем поймать li с помощью event.target. Напомню, что такое event.target - это именно тот тег, в котором случился клик, в нашем случае это li.
Итак, вот решение нашей задачи через делегирование:
<ul id="ul">
<li>пункт 1</li>
<li>пункт 2</li>
<li>пункт 3</li>
<li>пункт 4</li>
<li>пункт 5</li>
</ul>
var ul = document.getElementById('ul');
ul.addEventListener('click', addSign);
function addSign() {
event.target.innerHTML = event.target.innerHTML + '!';
}
Результат выполнения кода:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
При этом наше решение будет работать автоматически даже для новых li, ведь событие навешено не на li, а на ul:
<ul id="ul">
<li>пункт 1</li>
<li>пункт 2</li>
<li>пункт 3</li>
<li>пункт 4</li>
<li>пункт 5</li>
</ul>
<button id="button">Добавить li</button>
var ul = document.getElementById('ul');
ul.addEventListener('click', addSign);
function addSign() {
event.target.innerHTML = event.target.innerHTML + '!';
}
//Реализация кнопочки добавления новой li:
var button = document.getElementById('button');
button.addEventListener('click', addLi);
function addLi() {
var li = document.createElement('li');
li.innerHTML = 'новая li';
ul.appendChild(li);
}
Нажмите на кнопочку для добавления li, а затем на эту новую li - она среагирует:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Наш код рабочий, однако не без недостатков. Давайте разберем эти недостатки и напишем более универсальное решение.
Универсальное делегирование событий
Недостаток нашего кода проявится в том случае, когда внутри li будут какие-то вложенные теги. В нашем случае пусть это будут теги i:
<ul id="ul">
<li>пункт курсив 1</li>
<li>пункт курсив 2</li>
<li>пункт курсив 3</li>
<li>пункт курсив 4</li>
<li>пункт курсив 5</li>
</ul>
В этом случае нажатие на i приведет к добавлению восклицательного знака в конец тега i, а не тега li, как мы хотели бы (если нажать на li вне курсива - то все будет ок):
<ul id="ul">
<li>пункт курсив 1</li>
<li>пункт курсив 2</li>
<li>пункт курсив 3</li>
<li>пункт курсив 4</li>
<li>пункт курсив 5</li>
</ul>
var ul = document.getElementById('ul');
ul.addEventListener('click', addSign);
function addSign() {
event.target.innerHTML = event.target.innerHTML + '!';
}
Нажмите на курсив - вы увидите как '!' добавится ему в конец (нажатие вне курсива будет работать нормально):
- пункт курсив 1
- пункт курсив 2
- пункт курсив 3
- пункт курсив 4
- пункт курсив 5
Проблема исправляется следующим образом (описанный способ не единственный, но самый простой): с помощью метода closest найдем ближайшую li, котоорая является родителем для event.target вот так: event.target.closest('li').
Как это работает: если клик был на i, то в event.target лежит этот i, а в event.target.closest('li') - наша li, для которой должно сработать событие.
Если же клик был на самой li, то и в event.target, и в event.target.closest('li') будет лежать наша li.
Давайте проверим:
<ul id="ul">
<li>пункт курсив 1</li>
<li>пункт курсив 2</li>
<li>пункт курсив 3</li>
<li>пункт курсив 4</li>
<li>пункт курсив 5</li>
</ul>
var ul = document.getElementById('ul');
ul.addEventListener('click', function(event) {
var li = event.target.closest('li');
if (li) { //проверяем, вдруг li-родителя вообще нет
li.innerHTML = li.innerHTML + '!';
}
});
Результат выполнения кода:
- пункт курсив 1
- пункт курсив 2
- пункт курсив 3
- пункт курсив 4
- пункт курсив 5
Не важно, какая глубина вложенности: тег i может лежать в теге b, а тот в теге span и только потом в li - это не имеет значения: конструкция event.target.closest('li') найдет родителя из любого уровня вложенности.
Дополнительные материалы
Рекомендую посмотреть тренинг по делегированию событий.