Pull to refresh

Events bubbling и events capturing

Reading time 5 min
Views 64K
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, при этом предоставляя похожий функционал для работы с событиями. Однако, понимание того, как это устроено изнутри, никогда не помешает :)

Материалы

Tags:
Hubs:
+81
Comments 21
Comments Comments 21

Articles