I think this example in the MDN docs should be able to answer your question well.
When an event is sent to a node in the DOM, the event goes through 4 phases, but I will just focus on the two that are the most relevant to your question:
- Capture phase
- Bubbling phase
Capture phase
When an event is sent to a listener attached to a DOM node, the event goes through the capture phase in which it traverses from the top of the DOM tree (aka Window), down to the parent of the element/target, notifying any interested listeners along the way. I'll come back to this in a moment. After this point, the capture phase is finished, and the event is delivered to the target's listener.
Bubbling phase
The bubbling phase only begins if the event was set to bubble. It starts with the parent of the target, until it reaches the root of the DOM. You can think of it as doing the same thing done in the capture phase, but in reverse, and not to the same set of listeners.
The other two phases I didn't focus on are the EMPTY/NONE, and the AT_TARGET phase. You can read about those at the given links
Now coming back to the capture phase; The only listeners notified during this phase are the ones that have specified the capture flag as part of the subscription. During the capture phase, only these listeners are notified. These listeners could also prevent the event from reaching the target by calling event.stopPropagation.
The capture flag is specified as the third argument to EventTarget.addEventListener. Omission of this argument is the same as false, thus allowing the event to also be captured by a non-target listener in the bubbling phase.
Likewise, in the bubbling phase, only those listeners who were not notified during the capture phase are notified this time. At any point, these listeners could stop the event from bubbling further up the tree by calling event.stopPropagation
Back to the question...
When I set an event to #inner to bubble, does Javascript check up to document or does it stop at #outer
During the capture phase, every parent element registered to receive that event in capture mode will be called, and that includes the document node. If #outer is also registered to receive the event in capture mode, and calls event.stopPropagation, then the event will stop at #outer before it reaches #inner. Likewise, if the event bubbles, only those listeners interested in the bubbling phase will be called.
Bubbling example
The following is an example of an event that bubbles, notice the order in which the listeners are called:
#inner Foo handler called #outer Foo handler called Document Foo handler called
BTW, this has nothing to do with the order the events were subscribed to
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("foo", (event) => { console.log("Document Foo handler called") }, false); document.getElementById("outer").addEventListener("foo", (event) => { console.log("#outer Foo handler called") }, false); const inner = document.getElementById('inner'); inner.addEventListener("foo", () => { console.log("#inner Foo handler called") }); setTimeout(() => { inner.dispatchEvent(new CustomEvent("foo", { bubbles: true })) }, Math.random() * 500); });
<div id='outer'> <div id='inner'>Hello</div> </div>
Capture example
Here is another example showing how we can prevent the event from reaching its target by calling stopPropagation from a parent element in capture mode. Notice the order:
Document Foo handler called #outer Foo handler called
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("foo", (event) => { console.log("Document Foo handler called") }, true); document.getElementById("outer").addEventListener("foo", (event) => { console.log("#outer Foo handler called"); event.stopPropagation(); }, true); const inner = document.getElementById('inner'); inner.addEventListener("foo", () => { console.log("#inner Foo handler called") }); setTimeout(() => { inner.dispatchEvent(new CustomEvent("foo", { bubbles: true })) }, Math.random() * 500); });
<div id='outer'> <div id='inner'>Hello</div> </div>
As you can see, although the event was sent to #inner, the event handler for #inner was never called because #outer called event.stopPropagation during the capture phase.
Finally, here is a slightly modified version of the MDN example, which shows how even custom events behave the same way.