Источник: http://learn.javascript.ru/event-delegation
Если у вас есть много элементов, события на которых нужно обрабатывать похожим образом, то не стоит присваивать отдельный обработчик каждому.
Вместо этого, назначьте один обработчик общему родителю. Из него можно получить целевой элемент event.target, понять на каком потомке произошло событие и обработать его.
Эта техника называется делегированием и очень активно применяется в современном JavaScript.
Делегирование событий позволяет удобно организовывать деревья и вложенные меню.
Давайте для начала обсудим одноуровневое меню:
<ul id="menu">
<li><a href="/php">PHP</a></li>
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/flash">Flash</a></li>
</ul>
Клики по пунктам меню будем обрабатывать при помощи JavaScript. Пунктов меню в примере всего несколько, но может быть и много. Конечно, можно назначить каждому пункту свой персональный onclick-обработчик, но что если пунктов 50, 100, или больше? Неужели нужно создавать столько обработчиков? Конечно же, нет!
Применим делегирование: назначим один обработчик для всего меню, а в нём уже разберёмся, где именно был клик и обработаем его:
Алгоритм:
event.target
.Код:
// 1. вешаем обработчик
document.getElementById('menu').onclick = function(e) {
// 2. получаем event.target
var event = e || window.event;
var target = event.target || event.srcElement;
// 3. проверим, интересует ли нас этот клик?
// если клик был не на ссылке, то нет
if (target.tagName != 'A') return;
// обработать клик по ссылке
var href = target.getAttribute('href');
alert(href); // в данном примере просто выводим
return false;
};
В примере выше можно было бы получить target
при помощи логических операторов, вот так:
document.getElementById('menu').onclick = function(e) {
var target = e && e.target || event.srcElement;
...
};
Работать этот код будет следующим образом:
e.target
. При этом сработает левая часть оператора ИЛИ ||.target
.Полный код примера: http://learn.javascript.ru/play/tutorial/browser/events/delegation/menu/index.html
Обычное меню при использовании делегирования легко и непринуждённо превращается во вложенное.
У вложенного меню остается похожая семантичная структура:
<ul id="menu">
<li><a href="/php">PHP</a>
<ul>
<li><a href="/php/manual">Справочник</a></li>
<li><a href="/php/snippets">Сниппеты</a></li>
</ul>
</li>
<li><a href="/html">HTML</a>
<ul>
<li><a href="/html/information">Информация</a></li>
<li><a href="/html/examples">Примеры</a></li>
</ul>
</li>
</ul>
С помощью CSS можно организовать скрытие вложенного списка UL
до того момента, пока соответствующий LI
не наведут курсор. Такое скрытие-появление элементов можно реализовать и при помощи JavaScript, но если что-то можно сделать в CSS — лучше использовать CSS.
Пример: http://learn.javascript.ru/play/tutorial/browser/events/delegation/menu-nested/index.html
Делегирование позволяет перейти от обычного меню к вложенному без добавления новых обработчиков.
Можно добавлять новые пункты меню или удалять ненужные. Так как применено делегирование, то обработчик подхватит новые элементы автоматически.
Теперь рассмотрим более сложный пример — диаграмму «Ба Гуа». Это таблица, отражающая древнюю китайскую философию.
Вот она:
Её HTML (схематично):
<table>
<tr>
<td>...<strong>Northwest</strong>...</td>
<td>...</td>
<td>...</td>
</tr>
<tr>...еще 2 строки такого же вида...</tr>
<tr>...еще 2 строки такого же вида...</tr>
</table>
Пример: http://learn.javascript.ru/play/tutorial/browser/events/delegation/bagua/index.html
В этом примере важно то, как реализована подсветка элементов — через делегирование.
Вместо того, чтобы назначать обработчик для каждой ячейки, назначен один обработчик для всей таблицы. Он использует event.target
, чтобы получить элемент, на котором произошло событие, и подсветить его.
Обратим внимание: клик может произойти на вложенном теге, внутри TD
. Например, на теге <STRONG>
. А затем он всплывает наверх:
В ячейках таблицы могут появиться и другие элементы. Это означает, что нельзя просто проверить target.tagName
.
Для того, чтобы найти TD, на котором был клик, нам нужно пройти вверх по цепочке родителей от target
. Если в процессе этого мы дойдём до TD
, то это означает, что клик был внутри этой ячейки.
Код:
table.onclick = function(event) {
event = event || window.event;
var target = event.target || event.srcElement; // (1)
while(target != this) { // (2)
if (target.tagName == 'TD') { // (3)
toggleHighlight(target);
break;
}
target = target.parentNode;
}
};
function highlight(node) {
if (highlightedCell) {
highlightedCell.style.backgroundColor = '';
}
highlightedCell = node;
node.style.backgroundColor = 'red';
}
В этом коде делается следующее:
event.target
(в IE<9: event.srcElement
).
Это может быть STRONG
или TD
. А возможно и такое, что клик попал в область между ячейками (если у таблицы задано расстояние между ячейками cellspacing
). В этом случае целевым элементом будет TR
или даже TABLE
.this
в обработчике — это элемент, на котором сработал обработчик, то есть сама таблица, так что проверка target != this
проверяет, дошли ли мы до неё. Так как сам обработчик стоит на таблице, то рано или поздно мы должны к ней прийти.TD
— стоп, эта та ячейка, внутри которой произошел клик. Она-то нам и нужна. Подсветим её и завершим цикл.В том случае, если клик был вне TD
, цикл while
просто дойдет до таблицы (рано или поздно, будет target == this
) и прекратится.
А теперь представьте себе, что в таблице не 9, а 1000 или 10.000 ячеек. Делегирование позволяет обойтись всего одним обработчиком для любого количества ячеек.
Ячейки таблицы и пункты меню — это примеры использования делегирования для обработки схожих элементов. Оно работает хорошо, поскольку действия для них примерно одинаковые.
Но делегирование позволяет использовать обработчик и для абсолютно разных действий.
Например, нам нужно сделать меню с разными кнопками: «Сохранить», «Загрузить», «Поиск» и т.д.
Первое, что может прийти в голову — это найти каждую кнопку и назначить ей свой обработчик.
Но более изящно решить задачу можно путем добавления одного обработчика на всё меню. Все клики внутри меню попадут в обработчик.
Но как нам узнать, какую кнопку нажали и как обработать это событие? Эту задачу мы можем решить, добавив каждой кнопке нужный нам метод в специальный атрибут, который назовем data-action
(можно придумать любое название, но data-* является валидным в HTML5):
<button data-action="save">Нажмите, чтобы Сохранить</button>
Обработчик считывает содержимое атрибута и выполняет метод. Взгляните на рабочий пример:
<div id="menu">
<button data-action="save">Нажмите, чтобы Сохранить</button>
<button data-action="load">Нажмите, чтобы Загрузить</button>
</div>
<script>
function Menu(elem) {
this.save = function() { alert('сохраняю'); };
this.load = function() { alert('загружаю'); };
var self = this;
elem.onclick = function(e) {
var target = e && e.target || event.srcElement; // (*)
var action = target.getAttribute('data-action');
if (action) {
self[action]();
}
};
}
new Menu(document.getElementById('menu'));
</script>
Обратите внимание, как используется трюк с var self = this
, чтобы сохранить ссылку на объект Menu
. Иначе обработчик просто бы не смог вызвать методы Menu
, потому что его собственный this
ссылается на элемент.
Что в этом случае нам дает использование делегирования событий?
action-save
, action-load
вместо data-action
. Обработчик найдёт класс action-*
и вызовет соответствующий метод. Это действительно очень удобно.Качество кода в примере выше можно повысить, если поменять названия методов объекта с save
, load
на onClickSave
, onClickLoad
, так как это не просто методы, а методы-обработчики событий. И вызывать их, соответственно, как self["onClick"+...]()
. Это сделает смысл методов более понятным и упростит чтение и поддержку кода.
Делегирование событий — это здорово! Пожалуй, это один из самых полезных приёмов для работы с DOM. Он отлично подходит, если есть много элементов, обработка которых очень схожа.
Алгоритм:
event.target
.target.parentNode
, пока не найдем нужный подходящий элемент (и обработаем его), или пока не упремся в контейнер (this
).Зачем использовать:
innerHTML
.Конечно, у делегирования событий есть свои ограничения.