Примечание: ниже перевод статьи "Event delegation without a JavaScript library", посвященной обзору методов по назначению обработчиков событий в JavaScript и их возможной оптимизации, которая дополнена моими комментариями и практической частью.
Большинство статей и примеров, которые я видел в последнее время по переопределению событий, основывались на какой-либо распространенной библиотеке. Например, в своей хорошо известной статье Chris Heilmann применяет YUI-библиотеку, а в прошлом месяце Dan Webb в своей презентации на @media использовал prototype.
Для тех из нас, кто вручную корпеет над достаточно запутанным JavaScript-приложением без использования этих замечательных библиотек, будет интересно взглянуть, как же на самом деле работает переопределение событий. Это звучит так, как будто изложенный ниже материал будет значительно сложнее обычного назначения обработчиков, но, на самом деле, все очень просто.
Небольшое отступление для разработчиков, которые еще не до конца поняли, о чем же будет идти речь в этой статье. Переопределение событий относится к методам уменьшения приемников событий (event listeners), которые назначены документу, путем определения одного приемника для контейнера и проверки в обработчике, из какого дочернего элемента всплыло (bubbled up) это событие.
Давайте рассмотрим пример с веб-сайта Multimap. Основная навигация по сайту включает 6 ссылок сверху, 4 из которым требуются обработчики событий, чтобы поменять у них атрибут href
. У этих 4 ссылок атрибут class
выставлен в bundle
.
Скорее всего, вы представите эту ситуацию следующим образом.
var MMNav = { init: function() { var nav = document.getElementById('mainNav'); var links = nav.getElementsByTagName('a'); for ( var i = 0, j = links.length; i < j; ++i ) { if ( links[i].className == 'bundle' ) { links[i].onclick = this.onclick; } } }, onclick: function() { this.href = this.href + '?name=value'; return true; } }
В этом фрагменте довольно много лишнего. Во-первых, метод getElementsByTagName
просматривает каждый дочерний DOM-узел в элементе mainNav
, чтобы найти все ссылки. Затем мы еще раз пробегаем по всему найденному массиву, чтобы проверить имя класса каждой ссылки. Это пустая трата процессорного времени на каждом этапе.
Как вы смотрите на то, чтобы прикрепить один-единственный обработчик событий к элементу mainNav
, чтобы затем отслеживать все клики на ссылки внутри него?
var MMNav = { init: function() { var nav = document.getElementById('mainNav'); nav.onclick = this.onclick; }, onclick: function(e) { if ( e.target.className == 'bundle' ) { e.target.href = e.target.href + '?name=value'; } return true; } }
Простота и элегантность данного подхода должны быть очевидны, но у него есть и некоторое количество преимуществ в плане производительности:
Есть одна небольшая проблема при использование изложенного выше кода. Определение целевого элемента у события, на самом деле, не является просто вызовом e.target
. В Internet Explorer необходимо использовать e.srcElement
. Самым простым решением для устранения этой проблемы является небольшая функция getEventTarget
. Ниже та версия, которую я использую.
function getEventTarget(e) { var e = e || window.event; var targ = e.target || e.srcElement; if (targ.nodeType == 3) { // боремся с Safari targ = targ.parentNode; } return targ; }
Переопределение событий в настоящее время является самой распространенной практикой, если речь заходит о большом числе обработчиков событий (например, о карте с сотнями точек, к которым назначены обработчики событий-кликов). По моему мнению, способ, которым это можно сделать, должен использоваться по умолчанию для добавления и обработки событий. Это должен быть простой, интуитивно понятный и хорошо оптимизированный метод для применения в качестве шаблона в программировании на стороне клиента, и он не должен требовать сотен строчек JavaScript-библиотек для своей работы.
Здесь заканчивается авторская статья и начинаются мои изыскания на тему.
Что, если нам нужно добавить такой обработчик на все ссылки (или почти на все)? Правильно, тогда для контейнера всех этих ссылок стоит выбрать document
. Ниже пример кода, который позволяет так сделать.
var MMNav = { init: function() { document.onclick = function(e) { var target = getEventTarget(e); if (target && target.className == 'bundle' ) { target.href += '?name=value'; } return true; }; } } function getEventTarget(e) { var e = e || window.event; var targ = e.target || e.srcElement; while (!targ.href || targ.nodeType == 3) { // боремся с Safari и вложенностью targ = targ.parentNode; } return targ; } window.onload=MMNav.init;
Немного комментариев:
nodeType
выставлялся всегда в 1. Возможно, это какие-то специфические случаи либо старые версии (кто-нибудь может подтвердить необходимость этого хака?)href
, то перебор продолжается, иначе возвращаем искомый объект. Вложение ссылок друг в друга запрещено стандартами, так что, если мы сами же проектируем HTML-код, то бояться нечего.init
все равно назначается только обработчик событий, то вынес его из отдельного метода в этот (также были замечены проблемы в Internet Explorer при использовании this
и window.onload
). В результате код стал меньше. Конечно, можно весь вызов init
вынести в правую часть выражения с window.onload
, но это уже будет на совести окончательного разработчика приложения.Приведенный код был опробован в Internet Explorer 5.5, 6, 7, Firefox 2.0.0.6, Opera 9.22 и Safari 3.0.3. Каких-либо проблем замечено не было. Большая просьба проверить в IE 5, у кого имеется такая возможность, и написать о поведении в более старых версиях браузеров.
Кроме заявленного уменьшения HTML-кода, улучшения его семантики, выделения логики работы с событиями в отдельный файл приведу некоторые цифры: на моем ноутбуке (Centrino Duo) один вызов getElementsByTagName
выполнялся в течение 15–60 мс (Opera — 16, IE — 30–40, Firefox — 40–60). Safari показал более интересные результаты: 365 (!) мс в первый раз и 0 — во все последующие. Для примера, генерация страницы на сервере занимает от нескольких до сотни мс. Цифры, собственно говоря, не нуждаются в комментариях.