Конспект JS-course

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

Источник: 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, или больше? Неужели нужно создавать столько обработчиков? Конечно же, нет!

Применим делегирование: назначим один обработчик для всего меню, а в нём уже разберёмся, где именно был клик и обработаем его:

Алгоритм:

  1. Вешаем обработчик на внешний элемент (меню).
  2. В обработчике: получаем event.target.
  3. В обработчике: смотрим, где именно был клик и обрабатываем его. Возможно и такое, что данный клик нас не интересует, например если он был на пустом месте.

Код:

// 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

В примере выше можно было бы получить target при помощи логических операторов, вот так:

document.getElementById('menu').onclick = function(e) {
  var target = e && e.target || event.srcElement;
  ...
};

Работать этот код будет следующим образом:

  1. Если это не IE<9, то есть первый аргумент e и свойство e.target. При этом сработает левая часть оператора ИЛИ ||.
  2. Если это старый IE, то первого аргумента нет, левая часть сразу становится false и вычисляется правая часть ИЛИ ||, которая в этом случае и является аналогом свойства 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. Например, на теге &lt;STRONG&gt;. А затем он всплывает наверх:

![](bagua.png)

В ячейках таблицы могут появиться и другие элементы. Это означает, что нельзя просто проверить 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';
}

В этом коде делается следующее:

  1. Мы кросс-браузерно получаем самый глубокий вложенный элемент, на котором произошло событие. Это event.target (в IE<9: event.srcElement). Это может быть STRONG или TD. А возможно и такое, что клик попал в область между ячейками (если у таблицы задано расстояние между ячейками cellspacing). В этом случае целевым элементом будет TR или даже TABLE.
  2. В цикле (2) мы поднимаемся вверх по цепочке родителей, пока не дойдем до таблицы. Значение this в обработчике — это элемент, на котором сработал обработчик, то есть сама таблица, так что проверка target != this проверяет, дошли ли мы до неё. Так как сам обработчик стоит на таблице, то рано или поздно мы должны к ней прийти.
  3. Если, поднимаясь вверх, мы дошли до 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 ссылается на элемент.

Что в этом случае нам дает использование делегирования событий?

  • Не нужно писать код, чтобы присвоить обработчик каждой кнопке. Меньше кода, меньше времени, потраченного на инициализацию.
  • Структура HTML становится по-настоящему гибкой. Мы можем добавлять/удалять кнопки в любое время.
  • Данный подход является семантичным. Мы можем использовать классы action-save, action-load вместо data-action. Обработчик найдёт класс action-* и вызовет соответствующий метод. Это действительно очень удобно.

Качество кода в примере выше можно повысить, если поменять названия методов объекта с save, load на onClickSave, onClickLoad, так как это не просто методы, а методы-обработчики событий. И вызывать их, соответственно, как self[&quot;onClick&quot;+...](). Это сделает смысл методов более понятным и упростит чтение и поддержку кода.

Итого

Делегирование событий — это здорово! Пожалуй, это один из самых полезных приёмов для работы с DOM. Он отлично подходит, если есть много элементов, обработка которых очень схожа.

Алгоритм:

  1. Вешаем обработчик на контейнер.
  2. В обработчике: получаем event.target.
  3. В обработчике: если необходимо, проходим вверх цепочку target.parentNode, пока не найдем нужный подходящий элемент (и обработаем его), или пока не упремся в контейнер (this).

Зачем использовать:

  • Упрощает инициализацию и экономит память: не нужно вешать много обработчиков.
  • Меньше кода: при добавлении и удалении элементов не нужно ставить или снимать обработчики.
  • Удобство изменений: можно массово добавлять или удалять элементы путём изменения innerHTML.

Конечно, у делегирования событий есть свои ограничения.

  • Во-первых, событие должно всплывать. Большинство событий всплывают, но не все.
  • Во-вторых, делегирование создает дополнительную нагрузку на браузер, ведь обработчик запускается, когда событие происходит в любом месте контейнера, не обязательно на элементах, которые нам интересны. Но обычно эта нагрузка невелика и не является проблемой.