avatarMoon

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

9828

Abstract

ring">"console.log('div')"</span>></span> <span class="hljs-tag"><<span class="hljs-name">p</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"console.log('p')"</span>></span> <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"console.log('span')"</span>></span> <span class="hljs-tag"></<span class="hljs-name">span</span>></span> <span class="hljs-tag"></<span class="hljs-name">p</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></pre></div><p id="fe8f">Once the span tag is clicked, the event callback <code>console.log('span')</code> is executed. Then, JavaScript looks up its ancestors. In this case, the p tag is the parent element of span, so its callback, <code>console.log('p')</code>, is executed. Then its parent’s callback function is executed. So, the order for invoking the callback functions is:</p><div id="209c"><pre><span class="hljs-selector-tag">span</span><span class="hljs-selector-tag">p</span><span class="hljs-selector-tag">div</span></pre></div><p id="0272">The event capturing, on the other hand, works differently. JavaScript “captures” the topmost event and goes all the way down to the child elements to see if there’s a callback.</p><p id="217b">To make JavaScript event capture the events, you should use <code>addEventListener</code>. There are two ways to set the callback function as a capturing function:</p><div id="f684"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'div'</span>), <span class="hljs-literal">true</span>); div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'div'</span>), { capture: <span class="hljs-literal">true</span> });</pre></div><p id="cd26">Both ways are okay. But if you pass an object to <code>addEventListener</code>, you can set more options — check them out <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters">here</a>.</p><div id="708e"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'div'</span>), <span class="hljs-literal">true</span>); p.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'p'</span>), <span class="hljs-literal">true</span>); span.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'span'</span>), <span class="hljs-literal">true</span>);</pre></div><p id="45d6">Once <code>span</code> is clicked, the order of printing is as follows.</p><div id="1163"><pre><span class="hljs-selector-tag">div</span><span class="hljs-selector-tag">p</span><span class="hljs-selector-tag">span</span></pre></div><ul><li>Bubbling: The innermost element → the second innermost element → … → the outermost element</li><li>Capturing: The outermost element → the second outermost element → … → the innermost element</li></ul><p id="3b6b">Guess the result of this clicking event.</p><div id="d705"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'div'</span>)); p.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'p'</span>), { capture: <span class="hljs-literal">true</span> }); span.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'span'</span>));</pre></div><p id="eb20"><code>div</code> and <code>span</code> use bubbling, and <code>p</code> uses capturing. All of those elements are in the same hierarchy. What would happen then? The answer is:</p><div id="73a0"><pre><span class="hljs-selector-tag">p</span><span class="hljs-selector-tag">span</span><span class="hljs-selector-tag">div</span></pre></div><p id="ed49">Why? Because capturing is triggered earlier than bubbling.</p><figure id="d015"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*onegobhmdOuULb5qoUsLkA.png"><figcaption>The image source is from <a href="https://www.w3.org/TR/uievents/#bubble-phase">W3C</a></figcaption></figure><p id="547a">At first, JavaScript triggers the event from the topmost element to all the way down to the element where the event has been triggered (<code>Target Phase(2)</code>), then goes to the outermost element again.</p><h1 id="5e31">Event Delegation</h1><p id="66ac">This is more about the performance. Imagine there are a hundred list elements. And whichever you click, you want to know it’s ID. How can you do that?</p><div id="fb2c"><pre><span class="hljs-keyword">const</span> ul = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myUL'</span>);</pre></div><div id="ad03"><pre><span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-number">100</span>; i += <span class="hljs-number">1</span>) { <span class="hljs-keyword">const</span> li = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">'li'</span>); li.<span class="hljs-property">textContent</span> = <span class="hljs-string">li-<span class="hljs-subst">${i}</span></span>; li.<span class="hljs-property">id</span> = <span class="hljs-string">li-<span class="hljs-subst">${i}</span></span>; li.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">e</span> =></span> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(e.<span class="hljs-property">target</span>.<span class="hljs-property">id</span>)); ul.<span class="hljs-title function_">appendChild</span>(li); }</pre></div><p id="d235">In this example, the event <code>click</code> has been registered a hundred times, to each <code>li</code>. But what if you have to make a thousand elements? Registering the same event callback to every element isn’t best practice when it comes to the performance.</p><p id="3c8c">You can fix this problem with event bubbling. Once any <code>li</code> is clicked, JavaScript looks for callback functions in <code>li</code> ’s callback list, then goes to the outer element and looks for callback functions again.</p><div id="362a"><pre><span class="hljs-selector-tag">li</span><span class="hljs-selector-tag">ul</span></pre></div><p id="904d">Then you can register the callback function to <code>ul</code> tag. Even though <code>li</code> wouldn’t have any event callback function, bubbling will get to the <code>ul</code> tag after all.</p><div id="04a7"><pre><span class="hljs-keyword">const</span> ul = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myUL'</span>);</pre></div><div id="4c23"><pre><span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-number">100</span>; i += <span class="hljs-number">1</span>) { <span class="hljs-keyword">const</span> li = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">createElement</span>(<span class="hljs-string">'li'</span>); li.<span class="hljs-property">textContent</span> = <span class="hljs-string">li-<span class="hljs-subst">${i}</span></span>; li.<span class="hljs-property">id</span> = <span class="hljs-string">li-<span class="hljs-subst">${i}</span></span>; ul.<span class="hljs-title function_">appendChild</span>(li); }</pre></div><div id="67f6"><pre>ul.addEventListener(<span class="hljs-string">'click'</span>, e => <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(e.target.id));</pre></div><p id="2f16">Now there is only one event callback function.</p><h1 id="4abc">Event Propagation</h1><p id="3a4b">Above, I explained that bubbling comes after capturing. But what if you don’t want the bubbling or capturing to be triggered?</p><p id="278e">Every event callback function takes an argument — the event object. It has several useful methods. The one for stopping propagating the events to the other elements is <code>stopPropagation</code>. It stops bubbling and capturing. Let’s look at an example.</p><div id="5da0"><pre><span class="hljs-tag"><<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">p</span>></span> <span class="hljs-tag"><<span class="hljs-name">span</span>></span>Click me<span class="hljs-tag"></<span class="hljs-name">span</span>></span> <span class="hljs-tag"></<span class="hljs-name">p</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></pre></div><figure id="4d89"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*6zv_DYfp_EpYPgDtfyX3qw.png"><figcaption></figcaption></figure><div id="d451"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class=

Options

"hljs-params">()</span> =></span> console.log(<span class="hljs-string">'div'</span>)); p.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'p'</span>)); span.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'span'</span>));</pre></div><p id="d7a1">If you click <code>p</code>, then the result will be <code>p → div</code> by bubbling. But if you use <code>stopPropagation</code>, the result will be different.</p><div id="2233"><pre>p.addEventListener(<span class="hljs-string">'click'</span>, e => { e.stopPropagation(); <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'p'</span>); })</pre></div><p id="c380">If you click the <code>p</code> tag again, the result will be just <code>p</code>, because <code>stopPropagation</code> “stopped” the event being bubbled up.</p><div id="be7b"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, () => <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'div'</span>), <span class="hljs-literal">true</span>); p.addEventListener(<span class="hljs-string">'click'</span>, e => { e.stopPropagation(); <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'p'</span>); }); span.addEventListener(<span class="hljs-string">'click'</span>, () => <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'span'</span>), <span class="hljs-literal">true</span>);</pre></div><p id="7027">Now, bubbling and capturing are mixed up. Click the <code>span</code> tag. The result will be <code>div → span → p</code>. Remember that capturing comes first. So, <code>div</code> is an ancestor element of <code>span</code> , and its callback is executed first. <code>p</code> has a callback function, but it isn’t for capturing. Then <code>span</code> ’s callback function is executed as the final capturing step. Now bubbling is triggered. <code>span</code> doesn’t have any callback functions registered for bubbling, so nothing happens. <code>p</code>, on the other hand, has a callback function for bubbling and it’s executed. Also, none of the callback functions of <code>div</code> are for bubbling, so nothing will be printed.</p><p id="a77f"><code>addEventListener</code> can add multiple callback functions.</p><div id="2317"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'div'</span>), <span class="hljs-literal">true</span>); p.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'p - capturing'</span>), <span class="hljs-literal">true</span>); p.addEventListener(<span class="hljs-string">'click'</span>, e => { e.stopPropagation(); console.log(<span class="hljs-string">'p - bubbling'</span>); }); span.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> console.log(<span class="hljs-string">'span'</span>), <span class="hljs-literal">true</span>);</pre></div><p id="7842">If <code>span</code> is clicked, the printing order will be <code>div → p-capturing → span → p-bubbling</code>.</p><p id="d8a1">Note that not every event can be bubbled. For example, <code>focus</code> and <code>blur</code> events don’t support bubbling. Hence, you should check ahead of time if an event you want to use support bubbling. You can check the <a href="https://www.w3.org/TR/uievents/#blur">W3C documentation</a> or <a href="https://developer.mozilla.org/en-US/docs/Web/Events#Focus_events">MDN documentation</a>.</p><h1 id="1166">Quiz Answer</h1><p id="25d0">Now, you can guess the right answer to this quiz I gave you at the beginning of this post:</p><div id="3eb3"><pre><span class="hljs-keyword">const</span> div = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myDiv'</span>); <span class="hljs-keyword">const</span> btn = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'myBtn'</span>);</pre></div><div id="2e99"><pre>const t = <span class="hljs-function"><span class="hljs-params">()</span> =></span> { setTimeout(<span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'setTimeout in 10ms'</span>), <span class="hljs-number">10</span>); requestAnimationFrame(<span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'rAF'</span>)); Promise.resolve().then(<span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(<span class="hljs-string">'Promise'</span>)); }</pre></div><div id="e682"><pre>div.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> { console.log(<span class="hljs-number">1</span>); t(); }); btn.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> { console.log(<span class="hljs-number">2</span>); t(); });</pre></div><p id="c5b6"><code>div</code> and <code>btn</code> doesn’t have a callback function for capturing. So now you know once <code>btn</code> is clicked, the order of the events will be <code>btn → div</code>. <code>2</code> will be printed first, obviously. And there are three different tasks in the function, <code>t</code>.</p><p id="1417">As I explained in the previous post, <i>Promise</i> is a microtask. Microtasks are only executed once the task queue is completely empty. After <code>console.log(2)</code> is executed, nothing is in the task queue, so <code>console.log('Promise')</code> is now executed. <i>setTimeout</i> and <i>rAF</i>, however, are macrotasks. They can be executed after all of the tasks and the microtasks are completely executed.</p><p id="5054">Then the event is bubbled up to <code>div</code>. And <code>console.log(1)</code> is executed. Then <i>Promise</i> is executed by the same reason as in <code>btn</code> . Then so far, the order is <code>2 → Promise → 1 → Promise</code>. And all of the tasks and the microtasks are dequeued. <i>rAF</i> is dequeued first because it’s always ahead of the rendering, unlike <i>setTimeout</i>. So the final answer is as follows.</p><div id="be2b"><pre><span class="hljs-symbol">2 </span>→ Promise → <span class="hljs-number">1</span> → Promise → rAF → rAF → setTimeout → setTimeout</pre></div><p id="b3db">However, the result will be different if you click <code>btn</code> with the JavaScript <code>click</code> method.</p><div id="3b65"><pre>btn<span class="hljs-selector-class">.click</span>()</pre></div><figure id="7da5"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*XcZnDK5RJ2nIJDSVdXZe8g.png"><figcaption></figcaption></figure><p id="6677">The left call stack is when you click <code>btn</code> on the browser, and the right call stack is when you click <code>btn</code> with JavaScript. Since JavaScript clicked the button, <code>script</code> is in the task queue. Then, it affects the result. Microtasks can only be executed when the task queue is completely empty, but because of <code>script</code>, the task queue won’t be empty until every task is executed first. So with the JavaScript click, the result will be as follows:</p><div id="b4ff"><pre><span class="hljs-symbol">2 </span><span class="hljs-number">1</span> → Promise → Promise → rAF → rAF → setTimeout → setTimeout</pre></div><h1 id="23ee">Conclusion</h1><p id="c19a">JavaScript event is very mystical and interesting. But once you understand how JavaScript events work, you can reduce mistakes and get rid of the fears.</p><p id="206f">One thing that has been interesting was that the DOM element’s event was different depending on how you occur the event. When you clicked the button with your mouse, JavaScript didn’t push “script” task into the task queue, however, it did when you clicked the button with JavaScript code, such as <code>btn.click()</code>. I hope you do your own experiment about this mystical issue!</p><h1 id="f9fd">More in this series</h1><ul><li><a href="https://readmedium.com/be-the-master-of-the-event-loop-in-javascript-part-1-6804cdf6608f">Read Be the Master of the Event Loop in JavaScript (Part 1)</a></li><li><a href="https://readmedium.com/be-the-master-of-the-event-loop-in-javascript-part-3-df51ab655c94">Read Be the Master of the Event Loop in JavaScript (Part 3)</a></li></ul><h1 id="b791">Resources</h1><ul><li><a href="https://www.w3.org/TR/uievents/#bubble-phase">Event Bubbling — W3C</a></li><li><a href="https://javascript.info/bubbling-and-capturing">Event Bubbling and Capturing — JavaScript.info</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/Events">Event References — MDN</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events">Introductions to Events — MDN</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener">addEventListener — MDN</a></li></ul></article></body>

Be the Master of the Event Loop in JavaScript (Part 2)

Event bubbling, capturing, and delegation

Photo by Jon Flobrant on Unsplash

In my previous post, I talked about the basic concept of the JavaScript event loop, the queues, and how the tasks are executed by the JavaScript engine. If you haven’t read my previous post, I recommend you read it first, since this is part two of the event loop series.

Like the previous post, I will start this post with a quiz.

<body>
  <div id="myDiv">
    <button id="myBtn">Click me</button>
  </div>
</body>

Here, we have the DOM elements shaped like this.

const div = document.getElementById('myDiv');
const btn = document.getElementById('myBtn');
const t = () => {
  setTimeout(() => console.log('setTimeout in 10ms'), 10);
  requestAnimationFrame(() => console.log('rAF'));
  Promise.resolve().then(() => console.log('Promise'));
}
div.addEventListener('click', () => { console.log(1); t(); });
btn.addEventListener('click', () => { console.log(2); t(); });

Now, guess the answer.

If you don’t know the clear answer or if you feel like it’s quite confusing or tricky enough to give you a headache, you’re in the right post for a better understanding of the event loop!

In this post, we’ll cover:

  1. DOM events
  2. Event bubbling and capturing
  3. Event delegation
  4. Event propagation
  5. Quiz answer

DOM Event

Imagine there’s an element in body tag.

<div id="myDiv">Hello</div>

Every DOM element can have callbacks on events, such as a click or double click. The #myDiv element doesn’t have any callbacks connected to the event module. Even though there is nothing connected to its click event, the DOM module still looks into the queue of the callbacks once it’s clicked.

There are three ways to add your callback function(s) to the DOM element as the event callback.

<div onclick="console.log('div')">Hello</div>

This is an inline event callback. You can connect a callback function directly to the DOM. This method has a higher priority to be executed compared to the other methods.

div.onclick = () => console.log('div');

The second method is to bind a callback function directly to the onclick method of the DOM.

div.addEventListener('click', () => console.log('div'));

The last way is to use a DOM API, addEventListener . It takes two arguments — the name of an event, and the callback function once the event is fired. There are many different events (you can check them out here). Make sure to write the right name of the event type, since it’s a case-sensitive string.

This would probably be the most typical way to bind an event callback function because of these reasons:

  1. There is a counterpart function for addEventListener — it'sremoveEventListener.

You can pass the declared function to addEventListener instead of an anonymous function. Once the program gets bigger and more complex, you might want to control your events more delicately.

div.addEventListener('click', callbackOne);
div.addEventListener('click', callbackTwo);
-- Later --
div.removeEventListener(callbackOne);

div ends up having only one callback after it deregisters callbackOne .

2. Unlike binding a callback function right to the DOM’s method, such as onclick, you can register multiple callback functions to addEventListener .

// All of these callback functions
// are executed in first-register-first-execute order
// once the button is clicked
div.addEventListener('click', () => console.log(1));
div.addEventListener('click', () => console.log(2));
div.addEventListener('click', () => console.log(3));

3. Only addEventListener supports event bubbling and capturing. We will talk about these deeper in a little bit.

Event Bubbling and Capturing

In W3C(World Wide Web Consortium) documentation, the event bubbling is described as follows:

The process by which an event can be handled by one of the target’s ancestors after being handled by the event target. See the description of the bubble phase in the context of event flow for more details.

Like the name of this concept, bubbling, you can imagine a small water bubble going all the way up to the surface.

Photo by Paweł Czerwiński on Unsplash
<div onclick="console.log('div')">
  <p onclick="console.log('p')">
    <span onclick="console.log('span')">
    </span>
  </p>
</div>

Once the span tag is clicked, the event callback console.log('span') is executed. Then, JavaScript looks up its ancestors. In this case, the p tag is the parent element of span, so its callback, console.log('p'), is executed. Then its parent’s callback function is executed. So, the order for invoking the callback functions is:

spanpdiv

The event capturing, on the other hand, works differently. JavaScript “captures” the topmost event and goes all the way down to the child elements to see if there’s a callback.

To make JavaScript event capture the events, you should use addEventListener. There are two ways to set the callback function as a capturing function:

div.addEventListener('click', () => console.log('div'), true);
div.addEventListener('click', () => console.log('div'), { capture: true });

Both ways are okay. But if you pass an object to addEventListener, you can set more options — check them out here.

div.addEventListener('click', () => console.log('div'), true);
p.addEventListener('click', () => console.log('p'), true);
span.addEventListener('click', () => console.log('span'), true);

Once span is clicked, the order of printing is as follows.

divpspan
  • Bubbling: The innermost element → the second innermost element → … → the outermost element
  • Capturing: The outermost element → the second outermost element → … → the innermost element

Guess the result of this clicking event.

div.addEventListener('click', () => console.log('div'));
p.addEventListener('click', () => console.log('p'), { capture: true });
span.addEventListener('click', () => console.log('span'));

div and span use bubbling, and p uses capturing. All of those elements are in the same hierarchy. What would happen then? The answer is:

pspandiv

Why? Because capturing is triggered earlier than bubbling.

The image source is from W3C

At first, JavaScript triggers the event from the topmost element to all the way down to the element where the event has been triggered (Target Phase(2)), then goes to the outermost element again.

Event Delegation

This is more about the performance. Imagine there are a hundred list elements. And whichever you click, you want to know it’s ID. How can you do that?

const ul = document.getElementById('myUL');
for (let i = 0; i < 100; i += 1) {
  const li = document.createElement('li');
  li.textContent = `li-${i}`;
  li.id = `li-${i}`;
  li.addEventListener('click', e => console.log(e.target.id));
  ul.appendChild(li);
}

In this example, the event click has been registered a hundred times, to each li. But what if you have to make a thousand elements? Registering the same event callback to every element isn’t best practice when it comes to the performance.

You can fix this problem with event bubbling. Once any li is clicked, JavaScript looks for callback functions in li ’s callback list, then goes to the outer element and looks for callback functions again.

liul

Then you can register the callback function to ul tag. Even though li wouldn’t have any event callback function, bubbling will get to the ul tag after all.

const ul = document.getElementById('myUL');
for (let i = 0; i < 100; i += 1) {
  const li = document.createElement('li');
  li.textContent = `li-${i}`;
  li.id = `li-${i}`;
  ul.appendChild(li);
}
ul.addEventListener('click', e => console.log(e.target.id));

Now there is only one event callback function.

Event Propagation

Above, I explained that bubbling comes after capturing. But what if you don’t want the bubbling or capturing to be triggered?

Every event callback function takes an argument — the event object. It has several useful methods. The one for stopping propagating the events to the other elements is stopPropagation. It stops bubbling and capturing. Let’s look at an example.

<div>
  <p>
    <span>Click me</span>
  </p>
</div>
div.addEventListener('click', () => console.log('div'));
p.addEventListener('click', () => console.log('p'));
span.addEventListener('click', () => console.log('span'));

If you click p, then the result will be p → div by bubbling. But if you use stopPropagation, the result will be different.

p.addEventListener('click', e => {
  e.stopPropagation();
  console.log('p');
})

If you click the p tag again, the result will be just p, because stopPropagation “stopped” the event being bubbled up.

div.addEventListener('click', () => console.log('div'), true);
p.addEventListener('click', e => {
  e.stopPropagation();
  console.log('p');
});
span.addEventListener('click', () => console.log('span'), true);

Now, bubbling and capturing are mixed up. Click the span tag. The result will be div → span → p. Remember that capturing comes first. So, div is an ancestor element of span , and its callback is executed first. p has a callback function, but it isn’t for capturing. Then span ’s callback function is executed as the final capturing step. Now bubbling is triggered. span doesn’t have any callback functions registered for bubbling, so nothing happens. p, on the other hand, has a callback function for bubbling and it’s executed. Also, none of the callback functions of div are for bubbling, so nothing will be printed.

addEventListener can add multiple callback functions.

div.addEventListener('click', () => console.log('div'), true);
p.addEventListener('click', () => console.log('p - capturing'), true);
p.addEventListener('click', e => {
  e.stopPropagation();
  console.log('p - bubbling');
});
span.addEventListener('click', () => console.log('span'), true);

If span is clicked, the printing order will be div → p-capturing → span → p-bubbling.

Note that not every event can be bubbled. For example, focus and blur events don’t support bubbling. Hence, you should check ahead of time if an event you want to use support bubbling. You can check the W3C documentation or MDN documentation.

Quiz Answer

Now, you can guess the right answer to this quiz I gave you at the beginning of this post:

const div = document.getElementById('myDiv');
const btn = document.getElementById('myBtn');
const t = () => {
  setTimeout(() => console.log('setTimeout in 10ms'), 10);
  requestAnimationFrame(() => console.log('rAF'));
  Promise.resolve().then(() => console.log('Promise'));
}
div.addEventListener('click', () => { console.log(1); t(); });
btn.addEventListener('click', () => { console.log(2); t(); });

div and btn doesn’t have a callback function for capturing. So now you know once btn is clicked, the order of the events will be btn → div. 2 will be printed first, obviously. And there are three different tasks in the function, t.

As I explained in the previous post, Promise is a microtask. Microtasks are only executed once the task queue is completely empty. After console.log(2) is executed, nothing is in the task queue, so console.log('Promise') is now executed. setTimeout and rAF, however, are macrotasks. They can be executed after all of the tasks and the microtasks are completely executed.

Then the event is bubbled up to div. And console.log(1) is executed. Then Promise is executed by the same reason as in btn . Then so far, the order is 2 → Promise → 1 → Promise. And all of the tasks and the microtasks are dequeued. rAF is dequeued first because it’s always ahead of the rendering, unlike setTimeout. So the final answer is as follows.

2 → Promise → 1 → Promise → rAF → rAF → setTimeout → setTimeout

However, the result will be different if you click btn with the JavaScript click method.

btn.click()

The left call stack is when you click btn on the browser, and the right call stack is when you click btn with JavaScript. Since JavaScript clicked the button, script is in the task queue. Then, it affects the result. Microtasks can only be executed when the task queue is completely empty, but because of script, the task queue won’t be empty until every task is executed first. So with the JavaScript click, the result will be as follows:

2 1 → Promise → Promise → rAF → rAF → setTimeout → setTimeout

Conclusion

JavaScript event is very mystical and interesting. But once you understand how JavaScript events work, you can reduce mistakes and get rid of the fears.

One thing that has been interesting was that the DOM element’s event was different depending on how you occur the event. When you clicked the button with your mouse, JavaScript didn’t push “script” task into the task queue, however, it did when you clicked the button with JavaScript code, such as btn.click(). I hope you do your own experiment about this mystical issue!

More in this series

Resources

JavaScript
Programming
Coding
Web Development
Nodejs
Recommended from ReadMedium