Pull to refresh

jQuery.live в деталях

Reading time 5 min
Views 33K
Ввиду недавнего обсуждения скорости работы jQuery.live и благоразумности писать свое собственное делегирование обработчиков событий, решил по полочкам разобрать работу jQuery.live. Т.е. целью данного топика поставлена задача выявления всех особенностей при использовании live-биндера и анализ кода. Без сравнительных характеристик, без приведения оптимальных методов делегирования.

Принцип работы live основан на делегировании обработчиков событий.

Делегирование — паттерн, в основе которого лежат 2 принципа javascript: всплытие событий (event bubbling stage) и возможность определения элемента, отследившего событие.

Тот факт, что делегирование отслеживает исключительно стадию всплытия события, объясняет невозможность обвешивания live-биндеров на события blur, focus, mouseenter, mouseleave, change и submit: все эти события не имеют стадий захвата и всплытия.



Рассмотрим простейший пример:

  1. <html>
  2.   <head>
  3.     <script type='text/javascript' src='jquery-1.3.2.js'> </script>
  4.     <script type='text/javascript'>
  5.       var divOnClick = function(_e) {
  6.         console.log('div clicked');
  7.       }
  8.       $('.ololo').live('click', divOnClick);
  9.     </script>
  10.   </head>
  11.   <body>
  12.     <div class="ololo">
  13.       <p>ololo</p>
  14.     </div>
  15.   </body>
  16. </html>
* This source code was highlighted with Source Code Highlighter.


Посмотрим, что происходит при обвешивании события на элемент div

  1. live: function( type, fn ){
  2.   var proxy = jQuery.event.proxy( fn );
  3.   proxy.guid += this.selector + type;
  4.  
  5.   jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );
  6.  
  7.   return this;
  8. }
* This source code was highlighted with Source Code Highlighter.


Так выглядит сама функция live.

Сначала создается proxy-функция для последующего вызова в контексте оригинального объекта

  1. proxy = proxy || function(){ return fn.apply(this, arguments); };
* This source code was highlighted with Source Code Highlighter.


Отметим, что this здесь в контексте оригинального объекта, т.е. указывает на $('.ololo')

Возвращаемся к live. Проксирующей функции назначается уникальный guid.

Функция liveConvert специальным образом формирует имя события в пространстве имен live

  1. function liveConvert(type, selector){
  2.   return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
  3. }
* This source code was highlighted with Source Code Highlighter.


В примере функция вернет строку: live.click.`ololo

В этот момент происходит делегирование обработчика события (в примере click) объекту $(document). Т.е. теперь просто-напросто все события click будет ловить объект document, а не оригинальный элемент.

bind в данном случае работает в контексте $(document) и добавляет ему обработчик событий.

  1. bind: function( type, data, fn ) {
  2.   return type == "unload" ? this.one(type, data, fn) : this.each(function(){
  3.     jQuery.event.add( this, type, fn || data, fn && data );
  4.   });
  5. },
* This source code was highlighted with Source Code Highlighter.


Весь метод jQuery.event.add разбирать не будем, остановимся только на интересных нам местах.

  1. jQuery.event = {
  2.   add: function(elem, types, handler, data) {
  3.     ...
* This source code was highlighted with Source Code Highlighter.


Параметрами в этот метод подаются: $(document), «live.click.'ololo», ".ololo", и исходная проксированная функция.

  1. jQuery.each(types.split(/\s+/), function(index, type) {
  2.   var namespaces = type.split(".");
  3.   type = namespaces.shift();
  4.   ...
  5.     if ( jQuery.event.specialAll[type] )
  6.       jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
* This source code was highlighted with Source Code Highlighter.


Здесь выделяется namespace live и проверяется, нету ли значения по данному ключу в хеше jQuery.event.specialAll. Для live есть:

  1. specialAll: {
  2.   live: {
  3.     setup: function( selector, namespaces ){
  4.       jQuery.event.add( this, namespaces[0], liveHandler );
  5.     },
  6.     ...
* This source code was highlighted with Source Code Highlighter.


Здесь нам интересна только функция liveHandler, которая и объясняет все особенности метода jQuery.live. Приведу ее код полностью:

  1. function liveHandler( event ){
  2.   var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
  3.     stop = true,
  4.     elems = [];
  5.  
  6.   jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
  7.     if ( check.test(fn.type) ) {
  8.       var elem = jQuery(event.target).closest(fn.data)[0];
  9.       if ( elem )
  10.         elems.push({ elem: elem, fn: fn });
  11.     }
  12.   });
  13.  
  14.   elems.sort(function(a,b) {
  15.     return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest");
  16.   });
  17.   
  18.   jQuery.each(elems, function(){
  19.     if ( this.fn.call(this.elem, event, this.fn.data) === false )
  20.       return (stop = false);
  21.   });
  22.  
  23.   return stop;
  24. }
* This source code was highlighted with Source Code Highlighter.


Что она делает:
Прежде всего, создает простое регулярное выражение для проверки того, что поймали интересующее нас событие, для которого есть экземпляр проксированной функции. Нас в примере интересует событие click.

8я строчка листинга уже заставляет задуматься. Для объекта, который поймал событие, происходит поиск родительского элемента, для которого определена проксированная функция (если таковая имеется). В примере <p> ловит click и в liveHandler будет происходить всплытие до родительского объекта <div>.

В этом месте мы и видим, что во-первых, провешивать события лучше на детей чем на их родителей, чтобы процесс всплытия не затягивался. Кроме того, каждый раз новый родительский объект будет проверяться на соответствие селектору, в нашем случае ".ololo", где и выявляется узкое место для сложных селекторов.

Если событие click отслеживает какой-либо другой элемент, для которого нету обработчика, то для него всеравно будет выполняться поиск замыкающего родительского объекта на соответствие заданному селектору. Если например дочерний элемент имеет уровень вложенности 100, то скрипт произведет трассировку до родительского объекта, так и не сумев найти нужный элемент.

Далее просто вызывается проксированная функция-обработчик в контексте оригинального объекта.

Какой можно сделать вывод из того, что мы увидели в коде?

Прежде всего, если провешивается live-биндер для события, то это событие в любом случае будет отслеживаться элементом document. Каждый раз событие будет всплывать от поймавшего его элемента к родительскому, соответствующему селектору. Таким образом, не следует использовать live в больших dom-структурах, с большим количеством элементов и высоким уровнем вложенности.

Поскольку live подразумевает добавление новых элементов, соответствующих указанному селектору, то логично будет ограничить локацию их появления, скажем, если мы предполагаем появление новых <li>-элементов в родительском <ul>, то разумно делегировать обработку события ему.

Желающие могут сделать бенчмарки в зависимости от уровня элементов на странице, в ИЕ6 эти зависимости видны на глаз.

Думаю, об особенностях работы live рассказать получилось. Надеюсь, кому-то будет полезно: )
Tags:
Hubs:
+30
Comments 12
Comments Comments 12

Articles