Конспект JS-course

Всплытие и перехват

Источник: http://learn.javascript.ru/bubbling-and-capturing

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

Это происходит потому, что событие всплывает.

Например, этот обработчик для DIV сработает, если вы кликните по вложенному тегу EM или CODE:

<div onclick="alert('Обработчик для Div сработал!')">
  <em>Кликните на <code>EM</code>, сработает обработчик на <code>DIV</code></em>
</div>

Всплытие

Основной принцип всплытия:

После того, как событие сработает на самом вложенном элементе, оно также сработает на родителях, вверх по цепочке вложенности.

Например, есть 3 вложенных блока:

<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="example.css">

<div class="d1">1  <!-- внешний (topmost) -->
    <div class="d2">2
        <div class="d3">3 <!-- внутренний (innermost) -->
        </div>
    </div>
</div>

</body>
</html>

Всплытие гарантирует, что клик по внутреннему div 3 вызовет событие onclick сначала на внутреннем элементе 3, затем на элементе 2 и в конце концов на элементе 1.

Этот процесс называется всплытием, потому что события «всплывают» от внутреннего элемента вверх через родителей, подобно тому, как всплывает пузырек воздуха в воде.

Текущий элемент, this

Элемент, на котором сработал обработчик, доступен через this.

Например, повесим на клик по каждому DIV функцию highlight, которая подсвечивает текущий элемент:

<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="example.css">

<div class="d1" onclick="highlight(this)">1 
    <div class="d2" onclick="highlight(this)">2
        <div class="d3" onclick="highlight(this)">3
        </div>
    </div>
</div>

<script>
function highlight(elem) {
    elem.style.backgroundColor = 'yellow';
    alert(elem.className);
    elem.style.backgroundColor = '';
}
</script>

</body>
</html>

Демонстрация: http://learn.javascript.ru/files/tutorial/browser/events/bubbling/bubble/index.html

Также существует свойство объекта события event.currentTarget.

Оно имеет тот же смысл, что и this, поэтому с первого взгляда избыточно, но иногда гораздо удобнее получить текущий элемент из объекта события.

IE8- при назначении обработчика через attachEvent не передаёт this. Браузеры IE8- не предоставляют this при назначении через attachEvent. Также в них нет свойства event.currentTarget.

Если вы будете использовать фреймворк для работы с событиями, то это не важно, так как он всё исправит. А при разработке на чистом JS имеет смысл вешать обработчики через onсобытие: это и кросс-браузерно, и this всегда есть.

Целевой элемент, event.target

Самый глубокий элемент, который вызывает событие, называется «целевым» или «исходным» элементом.

В IE<9 он доступен как event.srcElement, остальные браузеры используют event.target. Кроссбраузерное решение выглядит так:

var target = event.target || event.srcElement;
  • event.target/srcElement - означает исходный элемент, на котором произошло событие.
  • this - текущий элемент, до которого дошло всплытие и который запускает обработчик.

Демонстрация: http://learn.javascript.ru/files/tutorial/browser/events/bubbling/bubble-target/index.html

Прекращения всплытия

Всплытие идет прямо наверх. Обычно оно будет всплывать до &lt;HTML&gt;, а затем до document, вызывая все обработчики на своем пути.

Но любой промежуточный обработчик может решить, что событие полностью обработано, и остановить всплытие.

Сценарий, при котором это может быть нужно:

  1. На странице по правому клику показывается, при помощи JavaScript, специальное контекстное меню;
  2. На странице также есть таблица, которая показывает меню, но другое, своё;
  3. В случае правого клика по таблице её обработчик покажет меню и остановит всплытие, чтобы меню уровня страницы не показалось.

Код для остановки всплытия различается между IE<9 и остальными браузерами:

Стандартный код — это вызов метода:

event.stopPropagation()

Для IE<9 — это назначение свойства:

event.cancelBubble = true

Кросс-браузерное решение:

element.onclick = function(event) {
  event = event || window.event; // Кроссбраузерно получить событие

  if (event.stopPropagation) { // существует ли метод?
    // Стандартно:
    event.stopPropagation();
  } else {
    // Вариант IE
    event.cancelBubble = true;
  }
}

Есть еще вариант записи в одну строчку:

event.stopPropagation ? event.stopPropagation() : (event.cancelBubble=true);

Если у элемента есть несколько обработчиков на одно событие, то даже при прекращении всплытия все они будут выполнены.

Например, если на ссылке есть два onclick-обработчика, то остановка всплытия для одного из них никак не скажется на другом. Это логично, учитывая то, что, как мы уже говорили ранее, браузер не гарантирует взаимный порядок выполнения этих обработчиков. Они полностью независимы, и из одного нельзя отменить другой.

Всплытие – это удобно. Не прекращайте его без явной нужды, очевидной и архитектурно прозрачной.

Зачастую прекращение всплытия создаёт свои подводные камни, которые потом приходится обходить.

Например, вы для одного компонента интерфейса сделали stopPropagation на событие click. А позже, на совсем другом месте страницы понадобилось отследить «клик вне элемента» — скажем, чтобы закрыть пункт меню. Обычно для этого ставят обработчик document.onclick и по event.target проверяют, внутри был клик или нет. Но над областью, где клики убиваются stopPropagation, такой способ будет нерабочим!

Три стадии прохода событий

Во всех браузерах, кроме IE<9, есть три стадии прохода события.

Событие сначала идет сверху вниз. Эта стадия называется «стадия перехвата» (capturing stage). Событие достигло целевого элемента. Это — «стадия цели» (target stage). После этого событие начинает всплывать. Это — «стадия всплытия» (bubbling stage). Получается такая картина:

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

  • Если аргумент true, то событие будет перехвачено по дороге вниз.
  • Если аргумент false, то событие будет поймано при всплытии.

Стадия цели как-то особо не обрабатывается, но обработчики, назначаемые на стадии захвата и всплытия, срабатывают также на целевом элементе.

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

Код:

<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="example.css">

<div class="d1">1
    <div class="d2">2
        <div class="d3">3
        </div>
    </div>
</div>

<script>
var divs = document.getElementsByTagName('div');

// на каждый DIV повесить обработчик на стадии захвата
for(var i = 0; i < divs.length; i++) {
  divs[i].addEventListener("click", highlightThis, true);
}

function highlightThis() {
  this.style.backgroundColor = 'yellow';
  alert(this.className);
  this.style.backgroundColor = '';
}
</script>

</body>
</html>

Демонстрация: http://learn.javascript.ru/files/tutorial/browser/events/bubbling/capture/index.html

Чтобы назначить обработчики для обеих стадий, добавим новое событие: var divs = document.getElementsByTagName('div');

for (var i=0; i<divs.length; i++) {
  divs[i].addEventListener("click", highlightThis, true);
  divs[i].addEventListener("click", highlightThis, false);
}

Как видно из примера, один и тот же обработчик можно назначить на разные стадии. При этом номер текущей стадии он, при необходимости, может получить из свойства event.eventPhase.

Итого

  • Событие идет сначала сверху вниз к целевому элементу (стадия захвата), затем всплывает снизу вверх. В IE<9 стадия захвата отсутствует.
  • Все способы добавления обработчика используют стадию всплытия, кроме addEventListener с последним аргументом true.
  • Всплытие/захват можно остановить с помощью вызова event.stopPropagation(). В IE<9 нужно использовать для этого event.cancelBubble=true.