Это третья часть учебника по оптимизации JavaScript, и сегодня я хочу поговорить о событиях. Простите за длительный перерыв между статьями, надеюсь, оставшиеся части будут публиковаться регулярнее.
Сценарий: у вас есть элементы, и необходимо добавить какую-то функциональность к ним (например, когда пользовательно наводит мышку на элемент, или щелкает по элементам).
Это обычная задача в веб-разработке. И первая вещь, которую знает каждый,– это разный синтаксис подписки на события в Internet Explorer и Firefox: первый использует element.attachEvent, второй — element.addEventListeners. Поэтому Вам нужно добавлять проверки, какой браузер используется в каждом конкретном случае. В наиболее популярной библиотеке Prototype это уже стандартизировано, и Вы всегда можете использовать Event.observe. Давайте немного потестируем.
Я начну с подписки на события через определение браузера вручную:
for (var i = items.length; i--; ) {
if (items[i].addEventListener) {
items[i].addEventListener('click', e_onclick, false);
} else if (items[i].attachEvent) {
items[i].attachEvent('onclick', e_onclick);
}
}
// Detaching events
for (var i = items.length; i--; ) {
if (items[i].removeEventListener) {
items[i].removeEventListener('click', e_onclick, false);
} else if (items[i].detachEvent) {
items[i].detachEvent('onclick', e_onclick);
}
}
Этот подход показал лучшее время: 188 и 203 ms в Internet Explorer 6 и 7, 125 и 141 ms в Firefox 1.5 and 2.0, и 63 ms в Opera 9, но есть одна проблема — утечки памяти: Internet Explorer обычно забывает почистить память, используемую обработчиками событий, когда вы переходите на другую страницу. Поэтому все JavaScript-фреймворки, реализующие функции подписки/отписывания от событий, предоставляют возможность автоматического удаления обработчиков при переходе на другую страницу. Давайте потестируем их.
Код для библиотеки Prototype:
for (var i = items.length; i--; ) {
Event.observe(items[i], 'click', e_onclick, false);
}
// Detaching events
for (var i = items.length; i--; ) {
Event.stopObserving(items[i], 'click', e_onclick, false);
}
Это очень медленно, 6453 ms в Internet Explorer 6, и похоже на то, что все еще есть какие-то утечки памяти (становится все медленнее и медленнее со временем), и 365 — 653 ms в других браузерах.
Существует множество проблем и решений, связанных с подпиской на события (взгляните только на Advanced event registration models). Не так давно даже прошло соревнование PPK’s addEvent() Recoding Contest (но не вздумайте использовать решение победителя в своих приложениях, оно крайне неэффективно и приводит к утечкам памяти).
Вместо него я решил протестировать невероятное и дерзкое решение от Dean Edwards, которое даже не использует методы addeventListener/attachEvent, но является полностью кросс-браузерным!
Использованный код:
for (var i = items.length; i--; ) {
addEvent(items[i], 'click', e_onclick);
}
// Detaching events
for (var i = items.length; i--; ) {
removeEvent(items[i], 'click', e_onclick);
}
Результаты настолько же впечатляющие, как и при подписке на события вручную, этот подход невероятно быстр для использования в реальных приложениях. Безусловно, вы скажете: “Стоп, о чем ты говоришь? Я не хочу использовать дополнительные методы, ведь я уже использую Prototype в своем коде!” Остыньте, вам не нужно этого делать, просто скачайте библиотеку Low Pro (текущая версия — 0.4, не забывайте обновляться). Разработчики Ruby on Rails могут воспользоваться плагином UJS Rails Plugin в своих приложениях для улучшения производительности — он включает и оптимизации Low Pro.
| No | Method | IE 6 | IE 7 | FF 1.5 | FF 2.0 | Opera 9 |
|---|---|---|---|---|---|---|
| 1 | manually | 203 | 188 | 125 | 141 | 63 |
| 2 | prototype.js | 6453 | 653 | 547 | 469 | 365 |
| 3 | addEvent | 783 | 344 | 94 | 141 | 62 |
Вы можете посмотреть тест и получить собственные результаты производительности здесь.
Выводы
- Всегда используйте Low Pro с Prototype.js (конечно, если ваше приложение содержит продвинутую логику на стороне клиента).
- Подписывайтесь на события вручную, если нужно выжать максимальную скорость из вашего приложения (например, когда вы манипулируете сотнями элементов). Но не забывайте об утечках памяти!
- Избегайте подписки на события везде, где это возможно (например, используйте CSS селектор :hover вместо события onmouseover).
- Будьте начеку, возможно завтра кто-то опубликует другие подсказки по оптимизации производительности.
Ссылки на другие части
- Часть 1: Добавление элементов DOM в документ
- Часть 2: Применение стилей к элементам
- Часть 3: Подписка на события
- Часть 4: Анонимные функции (будет опубликовано скоро)
- Часть 5: Подписка на события по требованию (будет опубликовано скоро)
- Часть 6: Отображение и скрытие элементов (будет опубликовано скоро)
- Часть 7: Перечисление элементов коллекции (будет опубликовано скоро)
Русский
English
Thanks for really interesting post and for mentioning Low Pro.
Optimalizace JavaScriptu…
Dmitrij Štefljuk píše velmi zajímavý seriál o optimalizaci JavaScriptu. Doporučuji všem, co se zabývají JavaScriptem.
…
Great article,
One thing to take into consideration about applying events (apart from “how”) is “where”.
I mean, for example, it’s far better to attach ONE “mouseover” event to a whole “tbody” and capture the “td” source which fired it than to attach many of “mouseover”s to ALL “td”s…
So, for speed reasons it is crucial “where” do you attach your events…
Thank you, Enrique
You right, of course. It’s better to use one event observer for parent element instead of separate observers for each child node. Great tip!
[...] JavaScript optimization Part 3: Attaching events [...]
[...] Что лучше использовать при подписке на события - фреймвор или делать это вручную? ответ здесь [...]
очень много дельных советов можно почерпнуть из этой статьи
ps. OpenID не отработал
Спасибо за ссылку и за баг
Будем посмотреть
I’m sure also, that Your code using “manual” events setting would be little faster if you checking event’s attaching methods existence once. On attaching example:
if (items[0].addEventListener) {
for (var i = items.length; i--; ) {
items[i].addEventListener('click', e_onclick, false);
}
} else if (items[0].attachEvent) {
for (var i = items.length; i--; ) {
items[i].attachEvent('onclick', e_onclick);
}
}
Look also on brilliant idea in one of last slide of Dan Webb’s presentation here:
http://www.danwebb.net/2007/11/22/media-ajax
btw. thx for the link to Low Pro