Events bubbling и events capturing

    intro
    Представьте, что на странице есть два блока, и один вложен в другой, как это показано на рисунке. В разметке страницы это выглядит так:
       <div id="block_outer">
          <div id="block_inner"></div>
       </div>

    А теперь представьте, что к блоку #block_outer привязано событие onClickOuter, а к блоку #block_inner, соответственно, событие onClickInner. И ответьте на вопрос, как сделать так, чтобы при клике на блок #block_inner, событие onClickOuter не вызывалось? И будет ли оно вообще вызвано? И если будет, то в каком порядке события будут вызываться? И знаете ли вы, как работает метод jQuery.live или подобные в других библиотеках (events delegation в ExtJS, например)?


    Немного истории


    На заре цивилизации, когда динозавры бегали по планете, а античные ITшники использовали высеченные из камня смартфоны, в самом разгаре была война браузеров, мнение MS и Netscape по поводу поведения событий на веб-страницах разделилось (к счастью, мне в силу возраста не пришлось столкнуться с этим в те далекие времена). При вложенности элементов на странице (как в примере выше) MS предложила модель events bubbling, то есть порядок выполнения событий должен подниматься («булькать») вверх по структуре DOM-дерева. Netscape предложила противоположную модель, названную event capturing, при которой обработка событий должна спускаться по элементам («захватывать» их) вниз по DOM-дереву.

    compare


    W3C попытался объединить оба варианта — стандарт позволяет программисту самому задавать поведение событий на странице, используя третий параметр метода
       addEventListener(type, listener, useCapture)
    

    То есть при клике сначала будет происходить фаза «спуска», и будут вызываться события, привязанные с флагом useCapture = true, затем будет запущена фаза «подъема», и остальные события будут вызываться в порядке подъема по DOM-дереву. По умолчанию события всегда подписываются на bubbling фазу, то есть, при таком способе подписки на событие useCapture = false:
       elementNode.onclick = someMethod;
    

    general


    Как с этим работают браузеры сегодня?


    Метод addEventListener не существует в IE ниже 9й версии. Для этого используется attachEvent, у которого нет третьего аргумента, то есть события всегда будут «булькать» в IE, и многое описанное ниже для этого браузера не имеет никакого смысла. Все остальные браузеры реализуют addEventListener согласно спецификации от 2000 года без отступлений.

    Подводя итог вышесказанному, давайте напишем небольшой тест, который покажет, как можно управлять приоритетом выполнения событий:
    • Структура HTML:
         <div id="level1">
            <div id="level2">
               <div id="level3">
               </div>
            </div>
         </div>
    • Сценарий
         // using jQuery;
         jQuery.fn.addEvent = function(type, listener, useCapture) {
            var self = this.get(0);
            if (self.addEventListener) {
               self.addEventListener(type, listener, useCapture);
            } else if (self.attachEvent) {
               self.attachEvent('on' + type, listener);
            }
         }   

         var EventsFactory = function(logBox){
            this.createEvent = function(text){
               return function(e){
                  logBox.append(text + ' ');
               }
            }
         }
         var factory = new EventsFactory( $('#test') );
         
         $('#level1').addEvent('click', factory.createEvent(1), true);
         $('#level1').addEvent('click', factory.createEvent(1), false);
         $('#level2').addEvent('click', factory.createEvent(2), true);
         $('#level3').addEvent('click', factory.createEvent(3), false);
    • Демо

    При клике на блок #level3 цифры будут выведены в следующем порядке:
       1 2 3 1
    

    То есть блоки #level1 и #level2 подписаны на capturing фазу, а #level3 и #level1 (еще раз подписан) на bubbling фазу. Первой вызывается capturing фаза со спуском вниз по дереву, первым находится #level1, затем #level2, потом подходит очередь самого элемента #level3, и затем, при поднятии по DOM, опять подходит очередь элемента #level1. Internet Explorer покажет нам:
       3 2 1 1
    


    Как прекратить выполнение следующего события?


    Любое из привязанных событий может прекратить обход следующих элементов:
    function someMethod(e) {
       if (!e) {
          window.event.cancelBubble = true;
       } else if (e.stopPropagation) {
          e.stopPropagation();
       }
    }

    W3C модель описывает метод stopPropagation у объекта события, но Microsoft отличилась и тут, поэтому для IE необходимо обратиться к полю event.cancelBubble.

    Event target


    Как известно, существует возможность определить элемент страницы, инициировавший событие. У объекта события есть поле target, которое ссылается на элемент-инициатор. Это проще показать на примере:
    • Структура HTML:
         <div id="level1">
            <div id="level2">
               <div id="level3">
               </div>
            </div>
         </div>
      
    • Сценарий
         $('#level1').addEvent('click', function(e) {
            // MSIE "features"
            var target = e.target ? e.target : e.srcElement;
            if ( $(target).is('#level3') ) {
               $('#test').append('#level3 clicked');
            }
         }, false);
      
    • Демо

    Поясню, что здесь происходит — при любом клике внутри #level1 мы проверяем event target, и если инициатором является внутренний блок #level3, то выполняем некий код. Эта реализация вам ничего не напоминает? Примерно так работает jQuery.live: если элемента на странице не существует, но он в будущем появится, то к нему все равно можно привязать событие. При bubbling фазе любое событие достигает уровня document, который является общим родителем для всех элементов на странице, и именно к нему мы можем привязывать события, которые могут запускать или не запускать выполнение определенных функций в зависимости от event.target.

    И тут возникает вопрос: если jQuery.live привязывает события к bubbling фазе на уровне document, то значит предыдущие события могут прекратить цепочку вызовов и нарушить вызов последнего события? Да, это так, если одно из событий, выполняющихся до этого, вызовет event.stopPropagation(), то цепочка вызовов прервется. Вот пример:
    • Структура HTML:
         <div id="level1">
            <div id="level2">
               <div id="level3">
               </div>
            </div>
         </div>
      
    • Сценарий
         $('#level1').live('click', function(e){
            $('#test').append('#level1 live triggered');
         });
         
         $('#level2').bind('click', function(e){
            $('#test').append('this will break live event');
            if (e.stopPropagation) {
               e.stopPropagation();
            }
         });
      
    • Демо

    При клике на область #level3 будет выведено «this will break live event», то есть live-событие не будет выполнено. Прошу обратить внимание, что такой хак возможен, он может быть красивой реализацией чего-либо, а иногда может быть трудно (адски трудно) уловимой ошибкой.
    Также важно отметить, что в примере выше переменная «e» является инстансом jQuery.Event. Для IE у события нет метода stopPropagation, и необходимо устанавливать флаг event.cancelBubble = true, чтобы остановить bubbling в IE. Но jQuery элегантно решает эту проблему, подменяя этот метод на свой.

    Как работают с событиями различные JS библиотеки?


    В этом месте я оговорюсь, что существует масса библиотек, которые умеют работать с событиями, но мы рассмотрим только jQuery, MooTools, Dojo и ExtJS, так как статья и познания автора, к сожалению, не резиновые. Поэтому любителей обсуждать языки и фреймворки попрошу пройти мимо.
    • jQuery
      умеет работать с событиями через bind, который всегда привязывает события к bubbling фазе, но имеет третий параметр «preventBubble», который останавливает цепочку событий для данного обработчика. Также существуют обертки для него типа click, change и пр., и способы делегировать события: delegate, live.
    • MooTools
      умеет работать с событиями через addEvent, который может обрабатывать custom events. AddEvent также не позволяет указывать фазу обработки. Работать с делегированием событий можно с помощью pseude :relay.
    • Dojo
      использует для привязки событий connect (который тоже может останавливать цепочку событий, если указан параметр «dontFix») или behavior. Для делегирования можно использовать метод delegate.
    • ExtJS
      предоставляет, на мой взгляд, самый простой интерфейс для работы с событиями. Для метода on возможно передавать параметры в виде объекта, такие как, например, delay, stopPropagation, delegate или собственные аргументы.

    Как мы видим, все эти библиотеки идут на компромисс с кроссбраузерностью, и везде используют events bubbling, при этом предоставляя похожий функционал для работы с событиями. Однако, понимание того, как это устроено изнутри, никогда не помешает :)

    Материалы

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

    More
    Ads

    Comments 22

    • UFO just landed and posted this here
        +6
        Не в тему, но так исторически сложилось. 1993 год, война IE и Netscape. Тогда IE предоставлял свои реализации js (JScript) и победил в борьбе с Netscape. Это уже потом, когда в 1994 году была основана W3C и стали появляться стандарты, Netscape Navigator и Opera пошли путём этих стандартов, а Microsoft решила не начинать всё заново, а продолжила играть по своим правилам. Как мы сегодня видим, это был ошибочный путь, но в то время, ввиду массовости и популярности IE, Microsoft могла себе позволить роскошь играть по-своему.
          +3
          Думаю это обусловлено исторически. В войне браузеров MS победила, благодаря корпоративному рынку и демпингу, и стала практически абсолютным монополистом. И с чего ей тогда было слушать W3C, который в свою очередь всегда был и есть рекомендотором и консультационным органом, а не регулятором?

          MS вела свою независимую политику в стандартах как лидер отрасли. Но из-за ориентации больше на толстого клиента и корпоративного заказчика, она потеряла конкурентное преимущество IE перед open source и не очень проектами, которые развивались на основе рекомендаций W3C.

          Сейчас рынок изменился и MS теперь подстраивается под него, стараясь избавится от всего legacy IE lte 6. Вообщем Netscape на последок забил гвоздь в крышку гроба того IE опубликовав исходный код и дав толчок другим проектам.

          Так что это просто веха в истории, а не повод для ярлыков добра/зла
          +1
          Мне понравилось.
          window.event.cancelBubble = true;… e.stopPropagation(); — были моими любимыми действиями, чтобы прекратить пузыриться (как же трудно было остановить распространение событий...), пока не появился jQuery.
          Сразу аналогия всплыла (так, для справки) — во Flash/ActionScript аналогичная модель и теже приколы с событиями и их передачей и были такие же приколы с изменениями стандартов при переходе с ActionScript 2.0 на 3.0. У меня сложилось впечатление, что Adobe списывала с IE… (help по flash.events.Event и с картинками).

          Мне интересно — есть развитие этой темы с обработкой событий в будущем или больше ничего придумывать не будут?
            +1
            Модель событий во Flash при использовании AS3 как раз отлично воплощает стандарт WC3.
            Проход сверху вниз — фаза capturing
            Проход снизу вверх — фаза bubbling
            И той и той фазой можно управлять, как заранее, при создании обработчика, так и в процессе, при том довольно гибко.
              0
              Мне интересно — есть развитие этой темы с обработкой событий в будущем или больше ничего придумывать не будут?

              Пока можно только сказать, что появляются новые события в новых стандартах HTML5 event, такие как ononline, onoffline, ondragstart и т.п. Также есть развитие, связанное с появлением новых устройств, как например onorientationchange событие для iPhone. Саму модель обработки и привязки событий, насколько я знаю, пока изменять никто не собирается.
                0
                Ну разве что появился addEventListener в IE9, хехе.
                  +1
                  Спасибо за подробности.
                  +3
                  Сейчас все перемешалось. Люди не знают javascript но знают jquery. Не знают SQL но знают ORM.
                  O tempora! O mores!
                    +2
                    Почему же перемешалось? Прогресс человечества ведь от лени. Развиваются парадигмы программирования высокого уровня. Так и должно быть. Но это ни коем образом не отстраняет от ответственности знать хотя бы основы низших уровней.
                  +1
                  Всё это хорошо и подробно описано у Фленагана.
                  А вот обзор библиотек очень кстати!
                  • UFO just landed and posted this here
                      0
                      Спасибо за ссылку, хорошая статья, будем считать что я написал велосипед =)
                        –2
                        Ммм, я бы даже к велосипедам это не отнёс… Ничего нового то не сказали, относительно той статьи. Просто это базовые вещи js на мой взгляд, который тысячу раз уж описаны-переписаны.
                          +1
                          Это не совсем так. Я попытался сделать упор на то, как это реализовано в библиотеках. В этом действительно несложно разобраться самому, и ничего инновационного в моей статье нет, но, как показывает практика, многие люди не знают и этого. Хотя бы посмотрите на количество людей, добавивших статью в закладки.
                            0
                            А я считаю, такие материалы периодически должны публиковаться, дабы не терять тонус. В любом статья — это плюс.
                          0
                          Статья СУПЕР. Беглый взгляд впечатлил. Завтра на работе буду читать.
                          0
                          Internet Explorer покажет нам:
                          3 2 1 1

                          Стоит заметить, что речь про IE до 9й версии. В 9й появился addEventListener и работает так же как в других браузерах.
                            +1
                            в 3 варианте
                            $('#level3').live('click', function(e){

                            исправтьте на

                            $('#level1').live('click', function(e){

                            как в странице по ссылке «Демо»
                              0
                              Спасибо за внимательность
                              0
                              я на столько глубоко программирование не знаю, но пару дней назад надо было такую же проблему решить, чтоб событие onClick срабатывало только на нажатый объект.

                              Написал для себя такой код:
                              $("div").live({
                              	click:	function() {
                              		console.log($(this).attr('id'));
                              		return false;
                              		}
                              	});
                              

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