Search code examples
javascriptdom-eventsevent-bubblingevent-capturing

Bubbling and Capturing events order at event target


Is Bubbling and Capturing events order during Target phase defined anywhere?

Pretty much anywhere you look, you will find that first there is Capturing phase, then Bubbling phase. Some sources additionally mention Target phase as a separate stage.

However, sources mentioning Target phase do not specify the order of events during this phase. I assumed that events registered as Capturing will precede these registered as Bubbling. It turns out it is not always the case!

In fact - according to my small experiment results - event handler execution during Target phase depends solely on their registration order.

const extBox = document.querySelector('.external-box');
const intBox = document.querySelector('.internal-box');
const par = document.querySelector('p');

extBox.addEventListener('click', bubblingEvent, false);
intBox.addEventListener('click', bubblingEvent, false);
par.addEventListener('click', bubblingEvent, false);

par.addEventListener('click', capturingEvent, true);
extBox.addEventListener('click', capturingEvent, true);
intBox.addEventListener('click', capturingEvent, true);      
      
function bubblingEvent(event) {
  console.log(event.currentTarget.className + ': bubbling event, ' + getPhaseStr(event.eventPhase));
}
      
function capturingEvent(event) {
  console.log(event.currentTarget.className + ': capturing event, ' + getPhaseStr(event.eventPhase));
}

function getPhaseStr (eventPhase) {
  let eventPhaseStr;

  switch (eventPhase) {
    case 0:
      eventPhaseStr = 'NONE';
      break;
    case 1:
      eventPhaseStr = 'CAPTURING_PHASE';
      break;
    case 2:
      eventPhaseStr = 'AT_TARGET';
      break;
    case 3:
      eventPhaseStr = 'BUBBLING_PHASE';
      break;
    default:
      eventPhaseStr = 'ERROR';
  }

  return eventPhaseStr;
}
* {
  box-sizing: border-box;
}

.external-box, .internal-box {
  width: 50%;
  margin: 1rem;
  padding: 1rem;
}

.external-box {
  background-color: aquamarine;
}

.internal-box {
  background-color: blueviolet;
}

p {
  background-color: cornsilk;
  padding: 1rem;
}
<div class='external-box'>external-box
  <div class='internal-box'>internal-box
    <p class='par'>paragraph</p>
  </div>
</div>

In the above example we can see, that - during Target phase - event added for Bubbling phase is executed before the Capturing Phase. This is because I have registered events for the Bubbling phase first. If I register events for the Capturing phase first, and then Bubbling ones, the order will be "correct".

My question once again: is events order during Target phase defined anywhere?


Solution

  • You are correct. When an event listener is attached to an element, and the element is the target, it does not matter whether whether the event listener is set to activate during capturing or during bubbling. The official specification describes it here.

    1. For each listener in listeners, whose removed is false:

    2. If phase is "capturing" and listener’s capture is false, then continue.

    3. If phase is "bubbling" and listener’s capture is true, then continue.

    4. (eventually invoke attached listener)

    When at the target, the phase is neither capturing nor bubbling, as shown by how your getPhaseStr function parses the eventPhase. So neither 3. continue nor 4. continue are activated, so the listener eventually gets invoked, in the order in which the listeners were attached. (The listener order is preserved in the "event listener list", a list of listeners attached to a particular element)

    b1.addEventListener('click', () => console.log('bubbling'));
    b1.addEventListener('click', () => console.log('capturing'), true);
    
    b2.addEventListener('click', () => console.log('capturing'), true);
    b2.addEventListener('click', () => console.log('bubbling'));
    <button id="b1">click</button>
    <button id="b2">click</button>