# Делегирование событий Всплытие событий позволяет реализовать один из самых важных приёмов разработки -- *делегирование*. Он заключается в том, что если у нас есть много элементов, события на которых нужно обрабатывать похожим образом, то вместо того, чтобы назначать обработчик каждому -- мы ставим один обработчик на их общего предка. Из него можно получить целевой элемент `event.target`, понять на каком именно потомке произошло событие и обработать его. ## Пример "Ба Гуа" Рассмотрим пример -- диаграмму "Ба Гуа". Это таблица, отражающая древнюю китайскую философию. Вот она: [iframe height=350 src="bagua" edit link] Её HTML (схематично): ```html ...еще 2 строки такого же вида......еще 2 строки такого же вида...
Bagua Chart: Direction, Element, Color, Meaning
...Northwest... ... ...
``` В этой таблице всего 9 ячеек, но могло быть и 99, и даже 9999, не важно. **Наша задача -- реализовать подсветку ячейки `` при клике.** Вместо того, чтобы назначать обработчик для каждой ячейки, которых может быть очень много -- мы повесим *единый обработчик* на элемент ``. Он будет использовать `event.target`, чтобы получить элемент, на котором произошло событие, и подсветить его. Код будет таким: ```js var selectedTd; *!* table.onclick = function(event) { var target = event.target; // где был клик? if (target.tagName != 'TD') return; // не на TD? тогда не интересует highlight(target); // подсветить TD }; */!* function highlight(node) { if (selectedTd) { selectedTd.classList.remove('highlight'); } selectedTd = node; selectedTd.classList.add('highlight'); } ``` Такому коду нет разницы, сколько ячеек в таблице. Обработчик всё равно один. Я могу добавлять, удалять `
` из таблицы, менять их количество -- моя подсветка будет стабильно работать, так как обработчик стоит на ``. Однако, у текущей версии кода есть недостаток. **Клик может быть не на том теге, который нас интересует, а внутри него.** В нашем случае, если взглянуть на HTML таблицы внимательно, видно, что ячейка содержит вложенные теги, например ``: ```html ``` Естественно, клик может произойти внутри `
*!* Northwest */!* ...Metal..Silver..Elders... `, на элементе ``. Такой клик будет пойман единым обработчиком, но `target` у него будет не ``, а ``: Внутри обработчика `table.onclick` мы должны по `event.target` разобраться, в каком именно `` был клик. Для этого мы, используя ссылку `parentNode`, будем идти вверх по иерархии родителей от `event.target` и выше и проверять:
  • Если нашли `
`, значит это то что нужно.
  • Если дошли до элемента `table` и при этом `
  • ` не найден, то наверное клик был вне ``, например на элементе заголовка таблицы. Улучшенный обработчик `table.onclick` с циклом `while`, который этот делает: ```js table.onclick = function(event) { var target = event.target; // цикл двигается вверх от target к родителям до table while (target != table) { if (target.tagName == 'TD') { // нашли элемент, который нас интересует! highlight(target); return; } target = target.parentNode; } // возможна ситуация, когда клик был вне // если цикл дошёл до table и ничего не нашёл, // то обработчик просто заканчивает работу } ``` [smart] Кстати, в проверке `while` можно бы было использовать `this` вместо `table`: ```js while (target != this) { // ... } ``` Это тоже будет работать, так как в обработчике `table.onclick` значением `this` является текущий элемент, то есть `table`. [/smart] Можно для этого использовать и метод `closest`, при поддержке браузером: ```js table.onclick = function(event) { var target = event.target; var td = target.closest('td'); if (!td) return; // клик вне , не интересует // если клик на td, но вне этой таблицы (возможно при вложенных таблицах) // то не интересует if (!table.contains(td)) return; // нашли элемент, который нас интересует! highlight(td); } ``` ## Применение делегирования: действия в разметке Обычно делегирование -- это средство оптимизации интерфейса. Мы используем один обработчик для *схожих* действий на однотипных элементах. Выше мы это делали для обработки кликов на ``. **Но делегирование позволяет использовать обработчик и для абсолютно разных действий.** Например, нам нужно сделать меню с разными кнопками: "Сохранить", "Загрузить", "Поиск" и т.д. И есть объект с соответствующими методами: `save`, `load`, `search` и т.п... Первое, что может прийти в голову -- это найти каждую кнопку и назначить ей свой обработчик среди методов объекта. Но более изящно решить задачу можно путем добавления одного обработчика на всё меню, а для каждой кнопки в специальном атрибуте, который мы назовем `data-action` (можно придумать любое название, но `data-*` является валидным в HTML5), укажем, что она должна вызывать: ```html ``` Обработчик считывает содержимое атрибута и выполняет метод. Взгляните на рабочий пример: ```html ``` Обратите внимание, как используется трюк с `var self = this`, чтобы сохранить ссылку на объект `Menu`. Иначе обработчик просто бы не смог вызвать методы `Menu`, потому что его собственный `this` ссылается на элемент. Что в этом случае нам дает использование делегирования событий? [compare] +Не нужно писать код, чтобы присвоить обработчик каждой кнопке. Меньше кода, меньше времени, потраченного на инициализацию. +Структура HTML становится по-настоящему гибкой. Мы можем добавлять/удалять кнопки в любое время. +Данный подход является семантичным. Также можно использовать классы `.action-save`, `.action-load` вместо атрибута `data-action`. [/compare] ## Итого Делегирование событий -- это здорово! Пожалуй, это один из самых полезных приёмов для работы с DOM. Он отлично подходит, если есть много элементов, обработка которых очень схожа. Алгоритм:
    1. Вешаем обработчик на контейнер.
    2. В обработчике: получаем `event.target`.
    3. В обработчике: если `event.target` или один из его родителей в контейнере (`this`) -- интересующий нас элемент -- обработать его.
    Зачем использовать: [compare] +Упрощает инициализацию и экономит память: не нужно вешать много обработчиков. +Меньше кода: при добавлении и удалении элементов не нужно ставить или снимать обработчики. +Удобство изменений: можно массово добавлять или удалять элементы путём изменения `innerHTML`. [/compare] Конечно, у делегирования событий есть свои ограничения. [compare] -Во-первых, событие должно всплывать. Нельзя, чтобы какой-то промежуточный обработчик вызвал `event.stopPropagation()` до того, как событие доплывёт до нужного элемента. -Во-вторых, делегирование создает дополнительную нагрузку на браузер, ведь обработчик запускается, когда событие происходит в любом месте контейнера, не обязательно на элементах, которые нам интересны. Но обычно эта нагрузка настолько пустяковая, её даже не стоит принимать во внимание. [/compare]