jQuery.live в деталях

    Ввиду недавнего обсуждения скорости работы 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 рассказать получилось. Надеюсь, кому-то будет полезно: )
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

    • UFO just landed and posted this here
        +1
        Вы правильно советуете вешать событие на конкретный контейнер вместо документа, однако jQuery в последней стабильной версии не поддерживает задание контекста в .live(). Эта возможность появится в версии 1.3.3, а до тех пор я предпочитаю настраивать делегирование вручную. Кроме того, в версии 1.4 собираются реализовать делегирование событий focus, blur, change и submit.
          0
          Ой, я может быть непонятно написал. Я имел ввиду создание собственного делегирования без участия live в силу указанной вами причины, что live не принимает вторым аргументом контекстный элемент.
          0
          а как насчёт компонент? ;-)
          mojura.110mb.com/test/js-component/
            0
            По коду:
            var klass= arguments.callee — не соответствует strict mode
            var latencyClean= this.latencyClean= arg.latencyClean || arg.laency || 100 — тут опечатка, latency

            Интересное место, прокомментируйте пожалуйста:
            1. var attachIfLoaded= this.attachIfLoaded= function( el ){
            2.     var cur= el
            3.     do {
            4.       if( el.nextSibling ) return attach( el )
            5.     } while( cur= cur.parentNode )
            6.   }
            * This source code was highlighted with Source Code Highlighter.


            Цепочка вызовов также интересная.

            Вообще, тема не очень освещенная и крайне занятная, могли бы рассказать поподробней. Из недостатков сразу в глаза бросается тот факт, что IE6 например сума сойдет, если будет происходить модификация DOM до его полной загрузки. Т.е. например в колбеке происходит какое-либо преобразование, а событие возникает до построения DOM. Другие браузеры по личному опыту это худо-бедно переваривают, но идеологически это неверно. Получается, что мы ограничиваем себя областью применения такого метода.
              +1
              > var klass= arguments.callee — не соответствует strict mode
              зато позволяет не завязываться на конкретные имена

              > тут опечатка, latency
              угу, спасибо

              > Интересное место, прокомментируйте пожалуйста:
              проверяет есть ли после текущего элемента другие элементы. и если есть, то создаёт виджет. иначе — откладывает на потом.
              таким образом виджет создаётся только после полной загрузки элемента, что гарантирует, в частности, адэкватное поведение ие6 ;-)

                0
                С ростом популярности jQuery инициализация работы всех плагинов по факту загрузки документа стала де-факто парадигмой. $(document).ready пишут все и всегда. Поэтому, здесь я вижу место потенциальных коллизий плагинов. Нужно отслеживать очередность их применения. Но это скорей теоретическая опасная точка.

                Правда, напишите топик на эту тему, материал достойный.
                  0
                  с моей-то кармой? XD
              0
              Не в тему: в примере приведено выражение delete this.el, this.destroy. Оператор delete не поддерживает несколько «аргументов». В данном случае delete удаляет this.el, запятая выступает в качестве оператора, вся конструкция возвращает метод this.destroy.
                0
                хех, я надеялся на лучшее ^_^''
              0
              При динамической подгрузке html в div, без live не обойтись. Необходимо пользоваться функцией live, а не click: $('.dynamic_button').live('click', function() {});
                0
                $(document).click(function(ev) {if ($(ev.target).filter('a.class').length {… } )}))

                проще говоря делегирование

              Only users with full accounts can post comments. Log in, please.