Начиналось все с использования 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");
});
}
Выглядит неплохо… Но работать он не будет. По крайней мере, не так, как мы этого ожидаем.
Для остальных же кратко объясню - функция обработчика замыкается на переменную 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 мы создаем функцию и привязываем ее как обработчик события. Для трех кнопок использование памяти незначительное.
Но если мы имеем что-то подобное:
то компьютер, конечно, не взорвется от переполнения. Однако наше использование памяти далеко от идеального. Мы выделяем огромное ее количество, хотя можно обойтись и без этого. Давайте перепишем наш код еще раз, так, чтобы использовать одну функцию несколько раз.
Вместо того чтобы ссылаться на переменную 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.
Как же большинство из них работает? Когда пользователь нажимает на элемент, генерируется событие, чтобы оповестить приложение об этом. Путешествие каждого события происходит в три стадии:Событие создается снаружи документа и затем последовательно перемещается по DOM иерархии до target (целевого) элемента. Как только оно добралось до своей цели, событие тем же путем выбирается из DOM элемента.
Вот наш HTML шаблон:
- Button A
- Button B
- Button C
Когда пользователь нажимает на кнопку А, событие путешествует таким образом:
Начало
| #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
.
Упс! Теперь, когда мы кликаем на 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.
Если бы я хотел сделать из этого библиотеку или использовать код в разработке, я бы добавил пару вещей:
Функция-помощник для проверки удовлетворения объекта критериям в более унифицированном и функциональном виде. Вроде:
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
(От переводчика: мой первый, судите строго.)
Счастливого кодинга!
На этом уроке мы познакомимся с таким понятием как всплытие события, а также рассмотрим, как его можно прервать. Кроме этого выясним, какие ещё этапы (фазы) проходит событие, перед тем как начать всплывать.
Всплытие событияЕсли у некоторого элемента возникает событие, то оно начинает "всплывать", т.е. возникает у родителя, потом у прародителя и т.д.
Из этого следует, что событие, которое сгенерировал некоторый элемент, можно перехватить с помощью обработчика на родителе, прародителе и т.д.
Всплытие события (пузырька) продемонстрируем на следующем примере:
Заголовок
Некоторый очень важный текст
РазделНекоторый текст
Остальной текст
Напишем небольшой скрипт, с помощью которого добавим обработчик события " click " для всех элементов страницы, а также для объектов document и window .
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);},false); }; document.addEventListener("click",function() {console.log(this);},false); window.addEventListener("click",function() {console.log(this);},false); });
Создадим HTML-страницу и вставим в неё вышеприведённый HTML код. Сценарий, написанный на языке JavaScript, вставим перед закрывающим тегом body . После этого откроем только что созданную страницу в веб-браузере, нажмём клавишу F12 и перейдём в консоль. Теперь нажмём левой кнопкой мышкой в области, принадлежащей элементу strong , и посмотрим, как событие будет всплывать.
Как прервать всплытие событияВсплытие события (пузырька) можно прервать. В этом случае у вышестоящих (родительских) элементов, данное событие вызвано не будет. Метод, который предназначен для прекращения всплытия события (пузрька) называется stopPropagation() .
Например, изменим наш вышеприведённый пример таким образом, чтобы событие не всплывало выше body: document.addEventListener("DOMContentLoaded", function() { var allElements = document.getElementsByTagName("*"); for (var i=0; i
Бесспорно всплытие - это очень удобно и архитектурно прозрачно. Не прекращайте его без явной нужды.
Получение элемента, который вызвал обработчикДля того чтобы получить DOM-элемент (объект), который вызвал обработчик события, необходимо использовать ключевое слово this . Данное ключевое слово (this) доступно в обработчике только в том случае, если Вы подписались на событие с помощью JavaScript.
Например, выведем в консоль id элемента, который вызвал обработчик события:
Var myP = document.getElementById("myP"); myP.addEventListener("click",function(){ //получим DOM-элемент, который вызвал обработчик события - this //получим его id и выведем его в консоль console.log(this.id); });
Для получения текущего элемента также можно использовать свойство currentTarget (event.currentTarget).
Этапы (фазы) прохода событияПеред тем как события начинает всплывать (этап всплытия), оно предварительно проходит ещё 2 этапа:
- 1 этап - это этап погружения до элемента, сгенерировавшего событие. Т.е. на данном этапе происходит движение сверху вниз, т.е. от объекта window до элемента. Также данный этап ещё называют этапом перехвата.
- 2 этап - это этап достижение цели, т.е. элемента (объекта), сгенерировавшего событие.
С учётом всех этапов, которые проходит событие, получается следующая картина:
Изменим сценарий вышеприведённого примера следующим образом:
Document.addEventListener("DOMContentLoaded", function() { var allElements = document.getElementsByTagName("*"); for (var i=0; i
Третий параметр методов addEventListener и removeEventListener определяет этап, на котором будет поймано событие. Если данный параметр имеет значение true , то событие будет перехватываться на стадии погружения (перехвата) события. А если параметр имеет значение false , то событие будет перехватываться на этапе всплытия. Для обработки события на самой цели, можно использовать метод addEventListener как со значением false , так и со значением true .
Внимание: на стадии погружения (перехвата), события могут перехватывать только обработчики, добавленные с помощью метода addEventListener() . Обработчики, добавленные с помощью других способов (атрибута HTML или через JavaScript с помощью свойства on[событие]) могут перехватывать события только на стадии всплытия.
Получение элемента, который сгенерировал событиеДля того чтобы получить целевой элемент, т.е. элемент, который сгенерировал событие, необходимо использовать свойство target (event.target).
Рассмотрим вышеприведённый пример, в котором изменим содержимое элемента script на следующее:
Document.addEventListener("DOMContentLoaded", function() { var elementBody = document.body; elementBody.addEventListener("click",function(){ console.log(this.tagName + " - элемент, который вызвал обработчик"); console.log(event.currentTarget.tagName + " - элемент, который вызвал обработчик"); console.log(event.target.tagName + " - элемент, который сгенерировал событие"); },false); });
Продемонстрируем наш пример, кликнув левой кнопкой мыши в области, принадлежащей элементу strong:
Начиналось все с использования 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");
});
}
Выглядит неплохо… Но работать он не будет. По крайней мере, не так, как мы этого ожидаем.
Для остальных же кратко объясню - функция обработчика замыкается на переменную 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 мы создаем функцию и привязываем ее как обработчик события. Для трех кнопок использование памяти незначительное.
Но если мы имеем что-то подобное:
то компьютер, конечно, не взорвется от переполнения. Однако наше использование памяти далеко от идеального. Мы выделяем огромное ее количество, хотя можно обойтись и без этого. Давайте перепишем наш код еще раз, так, чтобы использовать одну функцию несколько раз.
Вместо того чтобы ссылаться на переменную 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.
Как же большинство из них работает? Когда пользователь нажимает на элемент, генерируется событие, чтобы оповестить приложение об этом. Путешествие каждого события происходит в три стадии:Событие создается снаружи документа и затем последовательно перемещается по DOM иерархии до target (целевого) элемента. Как только оно добралось до своей цели, событие тем же путем выбирается из DOM элемента.
Вот наш HTML шаблон:
- Button A
- Button B
- Button C
Когда пользователь нажимает на кнопку А, событие путешествует таким образом:
Начало
| #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
.
Упс! Теперь, когда мы кликаем на 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.
Если бы я хотел сделать из этого библиотеку или использовать код в разработке, я бы добавил пару вещей:
Функция-помощник для проверки удовлетворения объекта критериям в более унифицированном и функциональном виде. Вроде:
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
(От переводчика: мой первый, судите строго.)
Счастливого кодинга!
Перехват событияОдна из важных особенностей языка - перехват события. Если кто-то, к примеру, щелкает на кнопке, то вызывается программа обработки события onClick, соответствующая этой кнопке. С помощью обработки событий Вы можете добиться того, чтобы объект, соответсвующий вашему окну, документу или слою, перехватывал и обрабатывал событие еще до того, как для этой цели объектом указанной кнопки будет вызван обработчик событий. Точно так же объект вашего окна, документа или слоя может обрабатывать сигнал о событии еще до того, как он достигает своего обычного адресата.
Чтобы увидеть, для чего это может пригодиться, давайте рассмотрим следующий пример:
window.onclick= handle;
function handle(e) {
alert("Объект window перехватывает это событие!");
return true; // т.е. проследить ссылку
}
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() . Это выглядит следующим образом:
window.captureEvents(Event.CLICK);
window.onclick= handle;
function handle(e) {
document.links.handleEvent(e);
}
"Кликните" по этой ссылке
Вторая ссылка
Все сигналы о событиях Click, посылаются на обработку по второй ссылке - даже если Вы вовсе и не щелкнули ни по одной из ссылок!
Следующий скрипт демонстрирует, как Ваш скрипт может реагировать на сигналы о нажатии клавиш. Нажмите на какую-либо клавишу и посмотрите, как работает этот скрипт.
window.captureEvents(Event.KEYPRESS);
window.onkeypress= pressed;
function pressed(e) {
alert("Key pressed! ASCII-value: " + e.which);
}
Сейчас мы с вами разберем некоторые продвинутые вещи при работе с объектом Event, а именно: всплытие и перехват, а также делегирование событий.
Всплытие событийПредставьте себе, что у вас есть несколько вложенных друг в друга блоков:
самый внутренний блок
Когда вы кликаете на самый внутренний блок, событие onclick возникает сначала в нем, а затем срабатывает в его родителе, в родителе его родителя и так далее, пока не дойдет то тега body и далее до тега html (затем до document и до window ).
И это логично, ведь кликая на внутренний блок, вы одновременно кликаете на все внешние.
Давайте убедимся в этом на следующем примере: у нас есть 3 блока, к каждому из них привязано событие onclick:
Нажмите на самый внутренний красный блок - и вы увидите, как сначала сработает onclick красного блока, потом голубого, потом зеленого:
Такое поведение называется всплытием событий - по аналогии со всплытием пузырька воздуха со дна. Так же, как и пузырек, наш клик по внутреннему элементу как будто выплывает наверх, каждый раз срабатывая на более высоких блоках.
event.targetПусть у нас есть два элемента: div и абзац p, лежащий внутри этого дива. Пусть onlick мы привязали в диву:
Когда мы кликаем на этот див, мы можем попасть по абзацу, а можем попасть в место, где этого абзаца нет.
Как такое может быть - посмотрите на следующем примере: зеленый цвет - это наш див, а голубой - наш абзац:
Если кликнуть в зеленую часть - мы кликнем именно по диву, а если кликнуть на голубую часть - клик произойдет сначала по абзацу, а потом уже по диву. Но так как onclick привязан именно к диву - мы в общем-то присутствие абзаца можем и не заметить.
Однако, иногда нам хотелось бы знать - клик произошел непосредственно по диву или по его потомку абзацу. В этом нам поможет объект Event и его свойство event.target - в нем хранится именно тот элемент, в котором произошел клик.
В следующем примере у нас есть div , внутри него лежит p , а внутри него - span .
Давайте привяжем событие onclick самому верхнему элементу (диву) и будем кликать на разные элементы: на div, на p, на span. С помощью event.target получим самый нижний элемент, в котором случилось событие и выведем его название с помощью tagName .
Если кликнуть, к примеру, на span - то событие отловит наш div (ведь именно к нему привязан onclick), но в event.target будет лежать именно span :
Покликайте по разным блокам - вы увидите результат:
Прекращение всплытияИтак, вы уже знаете, что все события всплывают до самого верха (до тега html, а затем до document, а затем до window). Иногда есть нужда это всплытие остановить. Это может сделать любой элемент, через который всплывает событие. Для этого в коде элемента следует вызвать метод event.stopPropagation() .
В следующем примере клик по красному блоку сработает на нем самом, затем на голубом блоке и все - голубой блок прекращает дальнейшее всплытие и зеленый блок уже никак не отреагирует:
Кликните на красный блок - вы увидите результат:
ПогружениеКроме всплытия событий есть еще и погружение (по научному стадия перехвата ). Это значит, что событие сначала идет сверху вниз (стадия перехвата), доходит до нашего элемента (стадия цели) и только потом начинает всплывать (стадия всплытия).
Повесить обработчик события с учетом стадии перехвата можно только с помощью 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 ей в конец добавляется "!".
Давайте реализуем описанное:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Понажимайте на li - вы увидите, как им в конец добавляется "!":
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Пусть теперь у нас также есть кнопочка, по нажатию на которую в конец ul добавляется новая li с текстом "пункт". Нас ждет сюрприз: привязанное событие не будет работать для новых li! Убедимся в этом:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Нажмите на кнопочку для добавления li, а затем на эту новую li - она не среагирует:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Для решения проблемы можно в момент создания новой li повесить на нее функцию addSign через addEventListener. Давайте реализуем это:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
- пункт 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 .
Итак, вот решение нашей задачи через делегирование:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Результат выполнения кода:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
При этом наше решение будет работать автоматически даже для новых li , ведь событие навешено не на li, а на ul:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Нажмите на кнопочку для добавления li, а затем на эту новую li - она среагирует:
- пункт 1
- пункт 2
- пункт 3
- пункт 4
- пункт 5
Наш код рабочий, однако не без недостатков. Давайте разберем эти недостатки и напишем более универсальное решение.
Универсальное делегирование событийНедостаток нашего кода проявится в том случае, когда внутри li будут какие-то вложенные теги. В нашем случае пусть это будут теги i :
В этом случае нажатие на i приведет к добавлению восклицательного знака в конец тега i , а не тега li , как мы хотели бы (если нажать на li вне курсива - то все будет ок):
- пункт курсив 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.
Давайте проверим:
- пункт курсив 1
- пункт курсив 2
- пункт курсив 3
- пункт курсив 4
- пункт курсив 5
Результат выполнения кода:
Не важно, какая глубина вложенности: тег i может лежать в теге b , а тот в теге span и только потом в li - это не имеет значения: конструкция event.target.closest("li") найдет родителя из любого уровня вложенности.