JavaScript - Всплытие и погружение событий

Александр Мальцев
9.4K
0
JavaScript - Всплытие и погружение событий
Содержание:
  1. Распространение события
  2. Погружение и всплытие на примере
  3. Делегирование событий
  4. Прерывания всплытия или погружения события
  5. Комментарии

На этом уроке мы познакомимся с таким понятием как всплытие события, а также рассмотрим, как его можно прервать. Кроме этого выясним, какие ещё этапы (фазы) проходит событие, перед тем как начать всплывать.

Распространение события

Когда некоторый объект инициирует событие, то оно не просто возникает на нём, а распространяется в документе определённым образом.

Это распространение является двунаправленным: от window к целевому элементу и обратно.

Согласно стандарту, оно представляется в виде 3 фаз:

  1. Фаза погружения или захвата – от window к родителю цели (цель – это объект, который инициировал это событие).
  2. Фаза цели – событие на цели.
  3. Фаза всплытия – обратно, от родителя цели к window.

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

Фаза погружения события

На этой фазе могут быть вызваны только обработчики, которые были зарегистрированы посредством addEventListener с указанием аргументу capture значения true:

$element.addEventListener(..., ..., {capture: true, ...});
// или просто "true"
$element.addEventListener(..., ..., true);

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

По умолчанию аргумент capture имеет значение false. Если его не указать или установить равным false, то обработчик в этом случае будет работать на этапе всплытия.

Например, в следующем примере фаза погружения при клике на <img> будет представлять вот такую цепочку: windowdocumenthtmlbodyarticlesection.

<body>
  <article>
    <section></section>
    <section>
      <img src="..." alt="...">
    </section>
    <section></section>
  </article>
  <aside></aside>
</body>
Фаза погружения события

Пример, в котором обработаем событие click на фазе погружения:

const $element = document.querySelector('article');
// добавим обработчик к элементу article на фазе погружения
$element.addEventListener('click', function (e) {
  console.log(`Фаза: ${e.eventPhase}`);
  console.log(`Элемент, для которого запущен обработчик: <${e.currentTarget.tagName.toLowerCase()}>`);
  console.log(`Элемент, который инициировал событие click: <${e.target.tagName.toLowerCase()}>`);
}, true);
Добавление обработчика на фазе погружения события

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

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

Фаза цели

На этой фазе будут вызваны все обработчики, прикреплённые к целевому объекту, независимо от того каким способом они назначены.

В примере который рассмотрели выше фаза цели при клике на <img> это конечно сам элемент: img.

Фаза цели

Добавление обработчика посредством HTML-атрибута onclick:

...
<img src="..." alt="..." onclick="imageOnClick(event)">
...

<script>
function imageOnClick(e) {
  console.log(`Фаза: ${e.eventPhase}`);
  console.log(`Элемент, для которого запущен обработчик: <${e.currentTarget.tagName.toLowerCase()}>`);
  console.log(`Элемент, который инициировал событие click: <${e.target.tagName.toLowerCase()}>`);
}
</script>
Добавление обработчика к элементу

Добавление обработчика посредством свойства onclick:

...
<img src="..." alt="...">
...

<script>
const $element = document.querySelector('img');
// добавим обработчик к элементу посредством свойства onclick
$element.onclick = function (e) {
  console.log(`Фаза: ${e.eventPhase}`);
  console.log(`Элемент, для которого запущен обработчик: <${e.currentTarget.tagName.toLowerCase()}>`);
  console.log(`Элемент, который инициировал событие click: <${e.target.tagName.toLowerCase()}>`);
}
</script>

Добавление обработчика через addEventListener:

...
<img src="..." alt="...">
...

<script>
const $element = document.querySelector('img');
// добавим обработчик к элементу
$element.addEventListener('click', function (e) {
  console.log(`Фаза: ${e.eventPhase}`);
  console.log(`Элемент, для которого запущен обработчик: <${e.currentTarget.tagName.toLowerCase()}>`);
  console.log(`Элемент, который инициировал событие click: <${e.target.tagName.toLowerCase()}>`);
});
</script>

Фаза всплытия

На фазы всплытия будут вызываться обработчики, которые мы зарегистрировали через HTML-атрибут on{event}, свойство on{event} и addEventListenercapture равным false или без его указания).

В примере приведённом выше фаза всплытия при клике на <img> будет распространяться вот так: sectionarticlebodyhtmldocumentwindow.

Фаза всплытия события

Пример, в котором обработаем событие click на фазе всплытия:

const $element = document.querySelector('article');
// добавим обработчик к элементу article на фазе погружения
$element.addEventListener('click', function (e) {
  console.log(`Фаза: ${e.eventPhase}`);
  console.log(`Элемент, для которого запущен обработчик: <${e.currentTarget.tagName.toLowerCase()}>`);
  console.log(`Элемент, который инициировал событие click: <${e.target.tagName.toLowerCase()}>`);
});
Добавление обработчика к элементу на фазе всплытия

Погружение и всплытие на примере

Рассмотрим следующий пример:

<body>
  <div id="level-1">
    <div id="level-2">
      <div id="level-3"></div>
    </div>
  </div>
</body>

При клике на элементе #level3 событие click начнёт путешествовать от window вниз по цепочке родителей до этого целевого элемента. Как только оно его достигнет, оно пойдёт вверх по цепочке родителей обратно до window.

Т.е. при клике по #level3:

  1. Фаза погружения: windowdocumenthtmlbody#level-1#level-2
  2. Фаза цели: #level-3
  3. Фаза всплытия: #level-2#level-1bodyhtmldocumentwindow

Чтобы программно посмотреть, как это происходит добавим обработчики ко всем участвующих в этом процессе элементам, а также к объектам document и window:

// обработчик события (выводит сообщение в консоль)
function log(e) { ... }

// получим все элементы на странице и добавим к ним обработчик события log на фазе погружения и всплытия
const $elements = document.querySelectorAll('*');
$elements.forEach(function ($element) {
  // на фазе всплытия
  $element.addEventListener('click', log);
  // на фазе погружения
  $element.addEventListener('click', log, true);
});

// добавляем обработчик к document на фазе всплытия
document.addEventListener('click', log);
// добавляем обработчик к document на фазе погружения
document.addEventListener('click', log, true);
// добавляем обработчик к window на фазе всплытия
window.addEventListener('click', log);
// добавляем обработчик к window на фазе погружения
window.addEventListener('click', log, true);
Распространение события в браузере

На скриншоте самый маленький по размеру квадрат - div#level-3, тот который побольше - div#level-2, а самый большой - div#level-1.

Обработчики на цели срабатывают как те, которые установлены на погружение, так и на всплытие. Поэтому у нас это сообщение «#level-3» вывелось 2 раза.

Получение номера фазы осуществляется с помощью свойства eventPhase (1 - погружение, 2 - цель, 3 - всплытие).

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

Делегирование событий – это механизм реагирования на событие через одного общего для этих элементов родителя.

Как это работает?

Делегирование возможно благодаря тому, что события всплывают. А это означает, что оно возникает не только на самом элементе, но затем на его родителе, потом на родителе его родителя и так далее вверх по цепочке до window.

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

Зачем это нужно?

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

Допустим, есть список:

<ul id="list">
  <li>Один</li>
  <li>Два</li>
  <li>Три</li>
</ul>

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

Мы можем добавить отдельный обработчик для каждого <li>. Но что, если у нас элементы <li> динамически добавляются и удаляются из этого списка?

В этом случае лучшим решением, конечно, будет добавить обработчик к родительскому элементу. Но если вы добавите обработчик к <ul>, то как вы узнаете, какой элемент был нажат?

Всё просто. Для этого предназначено свойство объекта события target. Оно содержит ссылку на элемент, на который фактически нажали.

Например:

document.querySelector ('#list').addEventListener('click', function(e) {
  if(e.target && e.target.nodeName === 'LI') {
    alert(e.target.textContent);
  }
});

Рассмотрим ещё один пример:

<button id="list-add">Добавить ещё в список</button>

<ul class="list">
  <li>
    <span class="hide">Первый список</span>
    <ul>
      <li>Один</li>
      <li>Два</li>
      <li>Три</li>
    </ul>
  </li>
  <li>
    <span class="hide">Второй список</span>
    <ul>
      <li>Четыре</li>
      <li>Пять</li>
      <li>Шесть</li>
    </ul>
  </li>
</ul>

<script>
  const $list = document.querySelector('.list');
  $list.addEventListener('click', function (e) {
    const $trigger = e.target.closest('span');
    if ($trigger) {
      $trigger.classList.toggle('hide');
    }
  });

  const $listAdd = document.querySelector('#list-add');
  $listAdd.addEventListener('click', function (e) {
    const $li = document.createElement('li');
    $li.innerHTML = '<span class="hide">Третий список</span><ul><li>Семь</li><li>Восемь</li><li>Девять</li></ul>'
    $list.appendChild($li);
  });
</script>

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

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

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

Например, изменим наш вышеприведённый пример таким образом, чтобы событие не всплывало выше body:

<script>
document.addEventListener("DOMContentLoaded", function() {
  var allElements = document.getElementsByTagName("*");
  for (var i=0; i < allElements.length; i++) {
    allElements[i].addEventListener("click",function(){
      console.log(this.tagName);
      if (this.tagName==="BODY")
        event.stopPropagation();
    },false);
  };
  document.addEventListener("click",function() {console.log(this);},false);
  window.addEventListener("click",function() {console.log(this);},false);
});
</script>
JavaScript - Прерывание всплытия события

Комментарии ()

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