Погружение и всплытие событий

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

<!DOCTYPE html>
<html>
<head>
<title>Уроки по JavaScript</title>
<style>
body {font-size: 20px;}
#first {background: #CC4444; width: 100px; padding: 10px}
#second {background: #00CC00; padding: 10px}
#third {background: #4444CC; padding: 0 10px 0 10px}
</style>
</head>
<body>
<div id="first">div
    <p id="second">p
        <span id="third">span</span>
    </p>
</div>
<script>
</script>
</body>
</html>

И мы на блок div вешаем обработчик:

onclick="showTag(event)"

который выводит в консоль имя тега элемента:

function showTag(event) {
     console.log(event.currentTarget.tagName);
}

Если мы теперь кликнем мышкой по любому из блоков, то сработает этот обработчик и в консоли отобразится «DIV». Почему так происходит? Дело в том, что события в браузерной среде, срабатывая на любом дочернем элементе, переходят к его родителю, и там тоже возникает такое же событие. И так далее до самого корневого объекта.

Это называется всплытием события. И мы можем в этом убедиться, назначив обработчик каждому тегу:

<div id="first" onclick="showTag(event)">div
    <p id="second" onclick="showTag(event)">p
        <span id="third" onclick="showTag(event)">span</span>
    </p>
</div>

Теперь, при обновлении документа, мы видим три тега в порядке возникновения события на каждом из них. Причем, это событие дойдет до самого корня DOM-дерева – до объекта document.

Так всплывают почти все события в JavaScript. Но есть исключения. Например, событие focus не всплывает. Но пока не заостряйте на этом свое внимание, просто имейте в виду, что всплытие – это не абсолютное правило, есть исключения. Если на каком-то уровне обработки нам необходимо узнать: от какого элемента пришло событие, то можно воспользоваться свойством

event.target

Например, перепишем обработчик с выводом этой информации в консоль:

function showTag(event) {
    console.log(event.currentTarget.tagName + 
          " от " + event.target.tagName);
}

Видите отличие между свойствами currentTarget и target: первое указывает на текущий элемент, в котором обрабатывается событие, а второе – на элемент, в котором изначально произошло событие.

Иногда возникает необходимость прервать процесс всплытия и остановить обработку событий. Это реализуется с помощью метода:

event.stopPropagation();

Добавим его в наш обработчик и убедимся, что событие теперь останавливается на том элементе, где и возникло.

Однако, если у элемента имеется несколько обработчиков, срабатывающих на одно и то же событие (как мы говорили на предыдущем занятии, они добавляются с помощью метода document.addEventListener(…)), то для остановки их всех следует использовать метод:

event.stopImmediatePropagation();

Так как с помощью stopPropagation останавливается только текущий обработчик, в котором он и был вызван.

Небольшая рекомендация начинающим программистам: не вызывайте без строгой необходимости методы остановки процесса всплытия события, так как ряд элементов имеют автоматические обработчики стандартных событий. Например, при клике по ссылке осуществляется переход по указанному адресу. Если событие не дойдет, то и перехода не будет. Так что, прерывая этот процесс, вы должны четко понимать, что делаете!

На самом деле, события, прежде чем всплыть, сначала погружаются. Вот картинка, взятая из спецификации языка JavaScript, которая представляет жизненный цикл события.

Смотрите, в первой фазе (красная линия) событие опускается до целевого элемента, по которому кликнули мышью (это ячейка таблицы – тег td). Достигнув элемента, событие переходит во вторую фазу. После этого начинается третья фаза – фаза всплытия (зеленая линия), то есть переход по родительским элементам.

Так вот, в JavaScript можно отслеживать события не только на стадии всплытия, но и на стадии погружения. Для этого обработчики следует добавлять с помощью метода addEventListener, у которого третий параметр устанавливается в значение true:

elem.addEventListener(..., {capture: true});

или просто писать true:

elem.addEventListener(..., true);

Перепишем наш скрипт в таком виде:

first.addEventListener("click", showTag, true);
second.addEventListener("click", showTag, true);
third.addEventListener("click", showTag, true);
 
function showTag(event) {
     console.log(event.eventPhase + ": " +
              event.currentTarget.tagName + 
              " от " + event.target.tagName); 
}

Теперь, при клике по элементу span, мы увидим как событие идет вниз от блока div и обратно от span к div. То есть, если в методе addEventListener третий аргумент равен true, то обработчик перехватывает событие в стадии погружения. Если же этот аргумент равен false – то на стадии всплытия. Кроме того, свойство eventPhase показывает нам номер фазы: 1 – погружение; 2 – достижение целевого элемента; 3 – всплытие. Причем, на целевом элементе span срабатывают два обработчика: и на погружение и на всплытие.

Если мы теперь захотим удалить какой-либо обработчик с помощью метода removeEventListener, то должны также указывать и фазу, для которой этот обработчик применен. Например, если записать вот так:

first.removeEventListener("click", showTag);

то обработчик showTag удален не будет, т.к. здесь по умолчанию идет удаление на всплытие. В нашем случае следует прописать так:

first.removeEventListener("click", showTag, true);

Теперь, при клике по span мы не увидим div на стадии погружения – обработчик был успешно удален.

Вот так в браузерной среде происходит движение большинства событий.

Видео по теме