Інтернет Windows Android

Введення в спливаючі події. JavaScript - Всплиття події Як же більшість з них працює

Вітаю! У цьому уроці я хочу розповісти про такий важливий понятті як спливання і перехоплення подій. Спливання це таке явище при якому, якщо ви натискаєте на дочірньому елементу, то подія поширюється і на його батько.

Буває дуже корисно при обробці великих вкладених списків або таблиць, щоб не призначати кожному елементу обробник події можна призначити один обробник на батьківський елемент, а подія вже буде поширюватися на всі вкладені елементи в батько. Давайте розглянемо на прикладі.

Цей оброблювач для

спрацює, якщо ви клацніть по вкладеному тегу або :

Натисніть на EM, спрацює обробник на DIV

Як бачите при кліці на вкладеному елементі em спрацьовує обробник на div. Чому так відбувається? Читайте далі і дізнаєтесь.

спливання

Отже основний принцип спливання:

При подію будь-якому байдуже клік мишкою наведення мишкою на елемент подія спочатку спрацює на батьківському е лементи, а потім по ланцюжку пошириться на всі вкладені елементи.

Наприклад, нехай є 3 вкладених елемента FORM> DIV> P, з обробником собьитія на кожному:

FORM
DIV

Спливання гарантує, що клік по внутрішньому елементу

Викличе обробник click (якщо він звичайно є) спочатку на самому

Такий процес називається випливанням, тому що події як би «спливають» від внутрішнього елемента вгору через своїх батьків, подібно до того, як спливає бульбашка повітря в воді, тому можна зустріти ще визначення бабблінг ну це просто від англійського слова bubbling - спливати.

Доступ до цільового елементу event.target

Для того, щоб дізнатися на якому саме елементі ми зловили ту чи іншу подію і існує метод event.target. (Про об'єкті event читайте).

  • event.target- це власне вихідний елемент, на якому і відбулася подія.
  • this- це завжди поточний елемент, до якого дійшло спливання, і на ньому зараз виконується оброблювач.

Наприклад, якщо у вас встановлений тільки один обробник form.onclick, то він і «зловить» все кліки всередині форми. При цьому де б не був клік всередині - він все одно спливе до елемента

, На якому і спрацює вже обробник.

При цьому:

  • this(= Event.currentTarget) завжди буде сама форма, так як обробник спрацював саме на ній.
  • event.targetбуде містити посилання на конкретний елемент всередині форми, самий вкладений, на якому стався клік.

В принципі this може збігатися з event.target якщо кликнули по формі і в формі більше немає ніяких елементів.

припинення спливання

Як правило спливання події йде прямо наверх і доходить до кореневого об'єкта window.

Але є можливість зупинити спливання на якомусь проміжному елементі.

Для того, щоб зупинити спливання треба викликати метод event.stopPropagation ().

Розглянемо приклад, при кліці на кнопку обробник body.onclick не спрацює:



Якщо у елемента встановлено кілька обробників на одне і теж подія, то навіть при припиненні спливання всі вони будуть виконані.

Таким чином, stopPropagation буде перешкоджає поширенню події далі, але на елементі все обробники відпрацюють, а ось далі на наступному елементі вже немає.

Для того, щоб зупинити обробку на текщем елементі, браузери підтримують метод event.stopImmediatePropagation (). Цей метод не тільки запобіжить спливання, а й зупиняли обробку подій на поточному елементі.

занурення

У стандарті, крім «спливання» подій, є ще й «занурення».

Занурення на відміну від спливання менш затребуваною, але все ж знати про нього буде корисно.

Отже є 3 стадії проходу події:

  1. Подія відбувається згори вниз. Ця стадія називається «стадія перехоплення».
  2. Подія досягло конкретного елемента. Це - «стадія мети».
  3. Після всього подія починає спливати. Це - «стадія спливання».

У стандарті це продемонстровано так:

Таким образомь, при кліці на TD подія буде подорожувати по ланцюжку батьків спочатку вниз до елементу ( «занурюється»), а потім нагору ( «спливає»), по шляху соотвественно задіюючи обробники.

Вище я писав тільки про спливанні, тому що власне інші стадії, не використовуються і проходять непомітно для нас.

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

А Щоб зловити подія на стадії перехоплення, як раз і потрібно використовувати:

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

приклади

У прикладі на ,

,

Стоять ті ж обробники, що і раніше, але цього разу - на стадії занурення. Ну а щоб побачити перехоплення в дії, клікніть в ньому на елементі

Обробники спрацюють в порядку «зверху-вниз»: FORM → DIV → P.

JS-код тут такий:

Var elems = document.querySelectorAll ( "form, div, p"); // на кожен елемент повісимо обробник на стадії перехоплення for (var i = 0; i< elems.length; i++) { elems[i].addEventListener("click", highlightThis, true); }


Ніхто вам не заважає призначити обробники для обох стадій, ось так:

Var elems = document.querySelectorAll ( "form, div, p"); for (var i = 0; i< elems.length; i++) { elems[i].addEventListener("click", highlightThis, true); elems[i].addEventListener("click", highlightThis, false); }

Клацніть по внутрішньому елементу

Щоб побачити порядок проходу події:
Повинно бути FORM → DIV → P → P → DIV → FORM. Зауважимо, що елемент

Братиме участь в обох стадіях.

підсумки

  • При настанні події - елемент, на якому відбулася подія, позначається як event.target.
  • Подія спочатку рухається вниз від кореня документа до event.target, по шляху викликаючи обробники, поставлені через addEventListener (...., True).
  • Подія рухається від event.target вгору до початку документа, по дорозі воно викликає обробники, поставлені через addEventListener (...., False).

Кожен обробник матиме доступ до властивостей події:

  • event.target - найглибший елемент, на якому власне і відбулася подія.
  • event.currentTarget (= this) - елемент, на якому в даний момент спрацював самобработчік (до якого «дійшло» подія).
  • event.eventPhase - на якій фазі спрацював обробник події (занурення = 1, спливання = 3).

Спливання можна зупинити викликом методу event.stopPropagation (), але робити це не рекомендується, оскільки подія може вам знадобиться для самих неожіданни' цілей.

перехоплення події

Одна з важливих особливостей мови - перехоплення події. Якщо хтось, наприклад, клацає на кнопці, то викликається програма обробки події onClick, відповідна цій кнопці. За допомогою обробки подій Ви можете добитися того, щоб об'єкт, відповідний вашого вікна, документом або шару, перехоплював і обробляв подія ще до того, як для цієї мети об'єктом зазначеної кнопки буде викликаний оброблювач подій. Точно так же об'єкт вашого вікна, документа або шару може обробляти сигнал про подію ще до того, як він досягає свого звичайного адресата.
Щоб побачити, для чого це може стати в нагоді, давайте розглянемо наступний приклад:






Click on this link



Як видно, ми не вказуємо програми обробки подій в тезі . Замість цього ми пишемо

window.captureEvents (Event.CLICK);

з тим, щоб перехопити подія Clickоб'єктом window. Зазвичай об'єкт window не працює з подією Click. Однак, перехопивши, ми потім його переадресуємо в об'єкт window. Зауважимо, що в Event.CLICKфрагмент CLICKповинен писатися великими літерами. Якщо ж Ви хочете перехоплювати кілька подій, то Вам слід відокремити їх один від одного символами |. наприклад:

window.captureEvents (Event.CLICK | Event.MOVE);

Крім цього в функції handle (), Призначеної нами на роль обробника подій, ми користуємося інструкцією return true;. Насправді це означає, що браузер повинен обробити і саму посилання, після того, як завершиться виконання функції handle (). Якщо ж Ви напишете замість цього return false;, То на цьому все і закінчиться.

Якщо тепер в тезі Ви задасте програму обробки події onClick, То зрозумієте, що дана програма при виникненні даної події викликана вже не буде. І це не дивно, оскільки об'єкт window перехоплює сигнал про подію ще до того, як він досягає об'єкта link. Якщо ж Ви визначите функцію handle ()як

function handle (e) (
alert ( "The window object captured this event!");
window.routeEvent (e);
return true;
}

то комп'ютер буде перевіряти, чи визначено інші програми обробки подій для даного об'єкта. Мінлива e - це наш об'єкт Event, який передається функції обробки подій у вигляді аргументу.

Крім того, Ви можете безпосередньо послати сигнал про подію будь-якого об'єкта. Для цього Ви можете скористатися методом handleEvent (). Це виглядає наступним чином:



"Натисніть" за цим посиланням

onClick = "alert (" Оброблювач подій для другого заслання! ");"> Друге посилання

Всі сигнали про події Click, надсилаються на обробку по другій посиланням - навіть якщо Ви зовсім і не клацнули ні по одній з посилань!

Наступний скрипт демонструє, як Ваш скрипт може реагувати на сигнали про натискання клавіш. Натисніть на будь-яку клавішу і подивіться, як працює цей скрипт.




Напишем небольшой скрипт, с помощью которого добавим обработчик события " click " для всех элементов страницы, а также для объектов document и window .

Створимо HTML-сторінку і вставимо в неї вищенаведений HTML код. Сценарій, написаний на мові JavaScript, вставимо перед закриває тегом body. Після цього відкриємо щойно створену сторінку в веб-браузері, натиснемо клавішу F12 і перейдемо в консоль. Тепер натиснемо лівою кнопкою мишкою в області, що належить елементу strong, і подивимося, як подія буде спливати.

Як перервати спливання події

Спливання події (бульбашки) можна перервати. В цьому випадку у вищих (батьківських) елементів, дана подія викликано не буде. Метод, який призначений для припинення спливання події (пузрька) називається stopPropagation ().

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


Когда пользователь нажимает на кнопку А, событие путешествует таким образом:

Начало
| #document
| Фаза перехвата
| HTML
| BODY
| UL
| LI#li_1
| Кнопка А < - Событие возникает для целевого элемента
| Фаза всплывания
| LI#li_1
| UL
| BODY
| HTML
v #document

Заметьте, что мы можем проследить путь, по которому событие двигается до своего целевого элемента. В нашем случае для каждой нажатой кнопки мы можем быть уверены, что событие всплывет обратно, пройдя через своего родителя - ul элемент. Мы можем использовать это и реализовать всплывающие cобытия.

Всплывающие события

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

В качестве конкретного примера возьмем нашу панель инструментов:

Ul class="toolbar">


  • Теперь, зная, что любое нажатие на кнопке всплывет через элемент ul.toolbar , давайте прикрепим наш обработчик событий на него. К счастью, он у нас уже есть:

    Var toolbar = document.querySelector(".toolbar"); toolbar.addEventListener("click", function(e) { var button = e.target; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); });
    Теперь мы имеем намного более чистый код, и мы даже избавились от циклов! Заметьте однако, что мы заменили e.currentTarget на e.target . Причина кроется в том, что мы обрабатываем события на другом уровне.

    e.target - фактическая цель события, то, куда оно пробирается через DOM, и откуда потом будет всплывать.
    e.currentTarget - текущий элемент, который обрабатывает событие. В нашем случае, это ul.toolbar .

    Улучшенные всплывающие события

    В данный момент мы обрабатываем любое нажатие на каждый элемент, которое всплывает через ul.toolbar , но наше условие проверки слишком простое. Что произошло бы, если бы имели более сложный DOM, включащий в себя иконки и элементы, которые не были созданы для того, чтобы по ним кликали?


    Упс! Теперь, когда мы кликаем на li.separator или иконку, мы добавляем ему класс .active . Как минимум, это нехорошо. Нам нужен способ фильтровать события так, чтобы мы реагировали на нужный нам элемент.

    Создадим для этого небольшую функцию-помощника:

    Var delegate = function(criteria, listener) { return function(e) { var el = e.target; do { if (!criteria(el)) continue; e.delegateTarget = el; listener.apply(this, arguments); return; } while((el = el.parentNode)); }; };
    Наш помощник делает две вещи. Во-первых, он обходит каждый элемент и его родителей и проверят, удовлетворяют ли они условию, переданному в параметре criteria . Если элемент удовлетворяет - помощник добавляет объекту события поле, называемое delegateTarget , в котором хранится элемент, удовлевторяющий нашим условиям. И затем вызывает обработчик. Соответственно, если ни один элемент не удовлетворяет условию, ни один обработчик не будет вызван.

    Мы можем использовать это так:

    Var toolbar = document.querySelector(".toolbar"); var buttonsFilter = function(elem) { return elem.classList && elem.classList.contains("btn"); }; var buttonHandler = function(e) { var button = e.delegateTarget; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; toolbar.addEventListener("click", delegate(buttonsFilter, buttonHandler));
    То, что доктор прописал: один обработчик событий, прикрепленный к одному элементу, который делает всю работу. Но делает ее только для нужных нам элементов. И он отлично реагирует на добавление и удаление объектов из DOM.

    Подводя итоги

    Мы вкратце рассмотрели основы реализации делегирования (обработки всплывающих) событий на чистом JavaScript. Это хорошо тем, что нам не нужно генерировать и прикреплять кучу обработчиков для каждого элемента.

    Если бы я хотел сделать из этого библиотеку или использовать код в разработке, я бы добавил пару вещей:

    Функция-помощник для проверки удовлетворения объекта критериям в более унифицированном и функциональном виде. Вроде:

    Var criteria = { isElement: function(e) { return e instanceof HTMLElement; }, hasClass: function(cls) { return function(e) { return criteria.isElement(e) && e.classList.contains(cls); } } // Больше критериев };
    Частичное использование помощника так же было бы не лишним:

    Var partialDelgate = function(criteria) { return function(handler) { return delgate(criteria, handler); } };
    Оригинал статьи: Understanding Delegated JavaScript Events
    (От переводчика: мой первый, судите строго.)

    Счастливого кодинга!

    Начиналось все с использования JavaScript и классов.

    Однако у меня возникла проблема. Я хотел использовать так называемые Всплывающие События, но также я хотел минимизировать зависимости, которые мне пришлось бы внедрять. Я не хотел подключать библиотеки jQuery для «этого маленького теста», толькло для того, чтобы использовать всплывающие события.

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

    Окей, так в чем проблема?

    Рассмотрим простой пример:

    Предположим, имеется список кнопок. Каждый раз, когда я нажимаю на одну из них, она должна стать «активной». После повторного нажатия кнопка должна вернуться в исходное состояние.

    Начнем с HTML:


    Я мог бы использовать стандартный JavaScript обработчик событий вроде такого:

    For(var i = 0; i < buttons.length; i++) { var button = buttons[i]; button.addEventListener("click", function() { if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }); }
    Выглядит неплохо… Но работать он не будет. По крайней мере, не так, как мы этого ожидаем.

    Замыкания победили

    Для тех, кто немного знает функциональный JavaScript, проблема очевидна.

    Для остальных же кратко объясню - функция обработчика замыкается на переменную button . Однако это переменная одна, и перезаписывается каждую итерацию.

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

    Что нам нужно, так это отдельный контекст для каждой функции:

    Var buttons = document.querySelectorAll(".toolbar button"); var createToolbarButtonHandler = function(button) { return function() { if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; }; for(var i = 0; i < buttons.length; i++) { buttons[i].addEventListener("click", createToolBarButtonHandler(buttons[i])); }
    Намного лучше! А главное, правильно работает. Мы создали функцию createToolbarButtonHandle , которая возвращает обработчик события. Затем для каждой кнопки вешаем свой обработчик.

    Так в чем проблема?

    И выглядит хорошо, и работает. Несмотря на это, мы все еще можем сделать наш код лучше.

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

    Но если мы имеем что-то подобное:

    • // ... еще 997 элементов...

    то компьютер, конечно, не взорвется от переполнения. Однако наше использование памяти далеко от идеального. Мы выделяем огромное ее количество, хотя можно обойтись и без этого. Давайте перепишем наш код еще раз, так, чтобы использовать одну функцию несколько раз.

    Вместо того чтобы ссылаться на переменную button , чтобы следить, на какую кнопку мы нажали, мы можем использовать event объект (объект «события»), который первым аргументом передается в каждый обработчик события.

    Event объект содержит некоторые данные о событии. В нашем случае нас интересует поле currentTarget . Из него мы получим ссылку на элемент, который был нажат:

    Var toolbarButtonHandler = function(e) { var button = e.currentTarget; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; for(var i = 0; i < buttons.length; i++) { button.addEventListener("click", toolbarButtonHandler); }
    Отлично! Мы не только упростили все до единственной функции, которая используется несколько раз, мы еще и сделали наш код более читаемым, удалив лишнюю функцию-генератор.

    Однако мы все еще можем лучше.

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

    Возможно, существует и другой подход?

    Начнем с того, что разберемся, как же работают события и как они двигаются по нашему DOM.

    Как же большинство из них работает?

    Когда пользователь нажимает на элемент, генерируется событие, чтобы оповестить приложение об этом. Путешествие каждого события происходит в три стадии:
    1. Фаза перехвата
    2. Событие возникает для целевого элемента
    3. Фаза всплывания
    Пометка: не все события проходят стадию перехвата или всплывания, некоторые создаются сразу на элементе. Однако это скорее исключение из правил.

    Событие создается снаружи документа и затем последовательно перемещается по DOM иерархии до target (целевого) элемента. Как только оно добралось до своей цели, событие тем же путем выбирается из DOM элемента.

    Вот наш HTML шаблон:




    Когда пользователь нажимает на кнопку А, событие путешествует таким образом:

    Начало
    | #document
    | Фаза перехвата
    | HTML
    | BODY
    | UL
    | LI#li_1
    | Кнопка А < - Событие возникает для целевого элемента
    | Фаза всплывания
    | LI#li_1
    | UL
    | BODY
    | HTML
    v #document

    Заметьте, что мы можем проследить путь, по которому событие двигается до своего целевого элемента. В нашем случае для каждой нажатой кнопки мы можем быть уверены, что событие всплывет обратно, пройдя через своего родителя - ul элемент. Мы можем использовать это и реализовать всплывающие cобытия.

    Всплывающие события

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

    В качестве конкретного примера возьмем нашу панель инструментов:

    Ul class="toolbar">


  • Теперь, зная, что любое нажатие на кнопке всплывет через элемент ul.toolbar , давайте прикрепим наш обработчик событий на него. К счастью, он у нас уже есть:

    Var toolbar = document.querySelector(".toolbar"); toolbar.addEventListener("click", function(e) { var button = e.target; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); });
    Теперь мы имеем намного более чистый код, и мы даже избавились от циклов! Заметьте однако, что мы заменили e.currentTarget на e.target . Причина кроется в том, что мы обрабатываем события на другом уровне.

    e.target - фактическая цель события, то, куда оно пробирается через DOM, и откуда потом будет всплывать.
    e.currentTarget - текущий элемент, который обрабатывает событие. В нашем случае, это ul.toolbar .

    Улучшенные всплывающие события

    В данный момент мы обрабатываем любое нажатие на каждый элемент, которое всплывает через ul.toolbar , но наше условие проверки слишком простое. Что произошло бы, если бы имели более сложный DOM, включащий в себя иконки и элементы, которые не были созданы для того, чтобы по ним кликали?


    Упс! Теперь, когда мы кликаем на li.separator или иконку, мы добавляем ему класс .active . Как минимум, это нехорошо. Нам нужен способ фильтровать события так, чтобы мы реагировали на нужный нам элемент.

    Создадим для этого небольшую функцию-помощника:

    Var delegate = function(criteria, listener) { return function(e) { var el = e.target; do { if (!criteria(el)) continue; e.delegateTarget = el; listener.apply(this, arguments); return; } while((el = el.parentNode)); }; };
    Наш помощник делает две вещи. Во-первых, он обходит каждый элемент и его родителей и проверят, удовлетворяют ли они условию, переданному в параметре criteria . Если элемент удовлетворяет - помощник добавляет объекту события поле, называемое delegateTarget , в котором хранится элемент, удовлевторяющий нашим условиям. И затем вызывает обработчик. Соответственно, если ни один элемент не удовлетворяет условию, ни один обработчик не будет вызван.

    Мы можем использовать это так:

    Var toolbar = document.querySelector(".toolbar"); var buttonsFilter = function(elem) { return elem.classList && elem.classList.contains("btn"); }; var buttonHandler = function(e) { var button = e.delegateTarget; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }; toolbar.addEventListener("click", delegate(buttonsFilter, buttonHandler));
    То, что доктор прописал: один обработчик событий, прикрепленный к одному элементу, который делает всю работу. Но делает ее только для нужных нам элементов. И он отлично реагирует на добавление и удаление объектов из DOM.

    Подводя итоги

    Мы вкратце рассмотрели основы реализации делегирования (обработки всплывающих) событий на чистом JavaScript. Это хорошо тем, что нам не нужно генерировать и прикреплять кучу обработчиков для каждого элемента.

    Если бы я хотел сделать из этого библиотеку или использовать код в разработке, я бы добавил пару вещей:

    Функция-помощник для проверки удовлетворения объекта критериям в более унифицированном и функциональном виде. Вроде:

    Var criteria = { isElement: function(e) { return e instanceof HTMLElement; }, hasClass: function(cls) { return function(e) { return criteria.isElement(e) && e.classList.contains(cls); } } // Больше критериев };
    Частичное использование помощника так же было бы не лишним:

    Var partialDelgate = function(criteria) { return function(handler) { return delgate(criteria, handler); } };
    Оригинал статьи: Understanding Delegated JavaScript Events
    (От переводчика: мой первый, судите строго.)

    Счастливого кодинга!