Статьи

Перевод: Николай Мациевский aka sunnybear
Опубликована: 23 августа 2007

Практический JS: ускоряем обработку событий

Примечание: ниже перевод статьи "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;
    }
}

Простота и элегантность данного подхода должны быть очевидны, но у него есть и некоторое количество преимуществ в плане производительности:

  • Чем меньше приемников событий прикреплено к документу, тем лучше. Они все загружаются в память и в чрезвычайных случаях могут сильно замедлить работу браузеров.
  • Загружается меньше кода на странице. Одной из главных проблем для сложных веб-приложений является задержка при загрузке JavaScript для исполнения и визуализации документа (прим. подробнее про оптимизацию времени загрузки документа можно посмотреть в этой статье). Два цикла из первого примера отсутствуют во втором.
  • «Исполнение по требованию». Второй пример выполняет немного больше действий, когда вызывается конечный обработчик событий, но это лучше, чем выполнять все действия при загрузке страницы, когда мы даже не знаем, будет ли запущен каждый конкретный обработчик событий (прим.: ссылок на странице может быть сотни, а пользователь нажмет только одну или две из них).
  • Меньше кода. Это означает уменьшение затрат на его поддержку и уменьшение трафика как с сервера, так и для ваших пользователей.

Боремся с Internet Explorer

Есть одна небольшая проблема при использование изложенного выше кода. Определение целевого элемента у события, на самом деле, не является просто вызовом 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;

Немного комментариев:

  • Заявленной проблемы с Safari не обнаружил (Safari 3.0.3 для Windows). 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 — во все последующие. Для примера, генерация страницы на сервере занимает от нескольких до сотни мс. Цифры, собственно говоря, не нуждаются в комментариях.

Читать дальше

Все комментарии (habrahabr.ru)