Pros and Cons of using e.stopPropagation() to prevent event bubbling
Asked Answered
R

3

8

Many people have explained that e.stopPropagation() prevents event bubbling. However, I'm having a hard time finding why one would want or want to prevent event bubbling in the first place.

On my site I have many elements which are called like this:

$(document.body).on('click', ".clickable", function(e){ 
   //e.stopPropagation();
  //do something, for example show a pop-up or click a link   
});

<body>
  <p>outside stuff</p>
  <button type="button" class='clickable'>
    <img src='/icon.jpg'> Do Something

  </button>
</body>

I was thinking to add e.stopPropagation() because I want to change the event handler to 'touch' from 'click' using this awesome touch library, Hammer.js.. This would allow for clicking to happen normally on desktop and for touch events on mobile devices.

The problem with this (please correct me if I'm wrong) is that scrolling on touch devices slows to a halt.

Is this where e.stopPropgation() is useful? Such that whenever one touches the screen document.body-event bubbling is NOT happening every time?

Racket answered 10/5, 2013 at 23:39 Comment(5)
You have return false in your handler, which already stops propagation...Vomit
I don't know what your question is. Is it: why would you want event bubbling? Is it: why would you not want event bubbling? Or is it: am I using stopPropagation right?Pretrice
@bažmegakapa oh yeah, thanks! ok this is a bad example, i need to correct...Racket
@FritsvanCampen yeah my question is why would you not want event bubbling? I guess one reason I was thinking is that it could lead to performance problems on touch devices because of the potential for too many rapid "click" events (fingers moving fast) that are each bubbling all the way up to the <body>.Racket
@timpeterson - I added a lot to my answer below related to the various issues you've raised in your comments.Frankfort
F
16

There are several ways you can handle events in javascript/jQuery. Two of them are:

  1. You can use a direct event handler on the object.
  2. You can use delegated event handling where you handle a propagated event on a parent.

If you are using a direct event handler on the object and there are no delegated event handlers configured in the page, then there is no reason for e.stopPropagation().

But, if you have delegated event handlers that are using propagation, you sometimes want to make sure that a higher level delegated event handler does not fire on the current event.

In your particular example:

$(document.body).on('click', "a.ajaxLink", function(e){ 

This is a delegated event handler. It is looking for any click event that propagates up to the document.body object, yet originated on an a.ajaxLink object. Here, there is little advantage to e.stopPropagation() because the event is almost entirely propagated already (it will also go up to the document, but unless you also have a handler for click on the document object, then there's no reason to e.stopPropagation() in this handler.

Where it would make a lot of sense would be when you have both a top level delegated event handler (like the one in your example) and you have lower level event handlers (either directly on the objects or using delegated event handling, but at a level below the document.body object. In that case, if you only want the lower level event handler to get the event, then you would call e.stopPropagation() in it's handler so that the document.body handler never saw the event.

$("a.ajaxLink").click(function(e) {
    if (some condition) {
        // do something specific to this condition
        code here
        // stop propagation so the default behavior for click in document.body does not fire
        e.stopPropagation();
    }
})

Note: Using return false from a jQuery event handler triggers both e.stopPropagation() and e.preventDefault(). But, if you are in a delegated event handler, the e.preventDefault() doesn't do anything because the default behavior (if there was one) has already fired when the target object first saw the event. Default behaviors happen before event propagation so e.preventDefault() only works in event handlers directly on the target object.


There is no noticeable performance degradation because you allow an event to bubble up because these are user level events and they just don't occur fast enough to matter, not is the bubbling particularly slow when there are no handlers on all the intervening objects. The system already special cases some events like mousemove that can happen rapidly to solve that issue. If you have a giant project with hundreds or thousands of event handlers, there are cases where using delegated event handling is more efficient and there are cases where direct event handlers on the actual target objects are more efficient. But, except in giant scenarios, the performance difference is probably not noticeable.

Here's an example where bubbling/delegation is more efficient. You have a giant table with thousands of rows and each row has two buttons in it (add/delete say). With delegated event handling, you can handle all the buttons in two simple event handlers that you attach to the table object (a common parent of the buttons). This will be much quicker to install the event handlers rather than installing several thousand event handlers directly on each and every button. These delegated event handlers will also automatically work on newly created rows/objects in the table. This is the ideal scenario for event bubbling/delegated event handlers. Note, in this scenario, there is no reason to stop propagation/bubbling.

Here's an example of where delegated event handlers are very inefficient. Supposed you have a good-sized web page with hundreds of objects and event handlers. You could make every one of the event handlers be a delegated event handler attached to the document object. But, here's what happens. A click happens. There's no event handler on the actual object so it bubbles up. Eventually, it gets to the document object. The document object has hundreds of event handlers. The event processing engine (jQuery in this case) has to look through every one of those event handlers and compare the selector in the delegated event handler for each one with the original event target to see if they match. Some of these comparisons are not fast as they can be full-blown CSS selectors. It has to do that for hundreds of delegated events. This is bad for performance. This is exactly why .live() in jQuery was deprecated because it worked this way. Instead, delegated event handlers should be placed as close to the target object as possible (the closest parent that is practical given the circumstances). And, when there is no need for a delegated event handler, handlers should be put on the actual target object as this is the most efficient at runtime.


Back to your original question. There is no time that you want bubbling turned off generally. As I described earlier in my answer, there are specific instances where an event handler further out on the tree wants to handle the event and stop any delegated event handlers higher up in the DOM tree from processing this event. That is a time to e.stopPropatation().


Here are several other relevant posts with useful info on this topic (as it has been widely discussed before):

Why not take Javascript event delegation to the extreme?

Should all jquery events be bound to $(document)?

Does jQuery.on() work for elements that are added after the event handler is created?

jQuery on() and stopPropagation()

Best practice to avoid memory or performance issues related to binding a large number of DOM objects to a click event

jQuery .live() vs .on() method for adding a click event after loading dynamic html

Frankfort answered 10/5, 2013 at 23:49 Comment(5)
@TimPeterson - I've added more to the end of my answer to address your question about whether you want to stop propagation to improve performance.Frankfort
-@jfriend00, thank you for your excellent answer. Ok i think I understand you. Can we discuss the last part, "Instead, delegated event handlers..."? So the best performance is using $('.ajaxLink').click(... And ideally I wouldn't use $(document.body).on('click', "a.ajaxLink",.. if I have hundreds of links but rather something closer to each link like this: $('#parentDiv').on('click', "a.ajaxLink",..? The major issue is that many of these links are created dynamically so I can't do direct event handling and have resorted to on() as a catch-all method. Thoughts?Racket
@timpeterson - it sounds like you have the general idea. For dynamically created objects, it is easiest to use delegated event handling, but you should attach the delegated event handler to the closest common parent object that is not itself dynamic (often some container object). What you don't want is lots of delegated event handlers all on the same high level object (e.g. document or document.body) if you can avoid it.Frankfort
@timpeterson - I added a bunch of relevant links to other posts on the same topic.Frankfort
-@jfriend00-this sounds good, thanks for your help. Learned alot.Racket
A
2

Imagine you have a button and want to handle a click event:

<button>
    <img src="icon" />
    <span>Text</span>
</button>

Without event propagation, clicking on the image or the text wouldn't trigger the click event handler bound to the button, as the event would never leave the <img> or the <span> elements.


A case where you don't want event propagation is where nested elements have their own event handlers. Here's one example:

<button>
    <img src="icon" />
    <span>Text</span>
    <span class="arrow">&darr;</span>
</button>

If you don't stop the event propagation with .arrow's event handler, it will trigger the button's event handler as well.

Acadia answered 10/5, 2013 at 23:44 Comment(11)
i get that but that's not what my (or I would think most people's) code looks like. In all cases I'm using classNames such as .ajaxLink that are named to trigger specfic handlers.Racket
@timpeterson: So what do you do if your element has children? I really doubt that you don't utilize event propagation.Acadia
I added some HTML to help us discuss because I'm confused on what you are saying. In my HTML above, I don't care what happens to the child <img>. Doesn't the propagation just go to <body>Racket
@timpeterson: Event propagation and event delegation are not the same thing.Acadia
-@Acadia I know they are not the same but doesn't my .ajaxLink click bubble all the way up to the <body>? What defines where it stops bubbling?Racket
@timpeterson: If you don't stop it with e.stopPropagation(), it'll bubble to the body. Your event handler will then check to see if e.target matches your selector, and if it does, the callback will be run.Acadia
-@Blender, I'm not sure why one would ever write code like you show in your answer. We know a button will submit a form unless it has the type="button" attribute so that seems like code you wouldn't want to write in the first place.Racket
@timpeterson: <button> doesn't submit a form. It's just a button. My example is similar to what Twitter Bootstrap does with its dropdown buttons. My point is that events propagate until you stop them, which may or may not be what you want to happen.Acadia
-@Blender, fair enough. I guess we are straying from my original question though. Does event bubbling carry a performance cost that could become real on touch devices?Racket
@timpeterson: It'd be helpful if you could make your question clearer. Now I'm not even sure what your question is.Acadia
When scrolling a page on a mobile device, you could conceivably trigger many click events if the page has many elements. Would turning off event bubbling help reduce the amount of work the browser has to do in this case. So this would be a reason NOT to want event bubbling?Racket
S
0

Do not use stopPropagation(), if possible.

Two advantages using stopPropagation() are:

  • It is easier to write code (independent event functions)
  • Performance

Although this function seems to be useful, it can be considered bad coding style, especially when you have no full control over the code (e. g. because you use a third-party library). stopPropagation() is a take-everything-or-nothing concept. It does not allow fine control flow. If some element between two other nested elements stops the event propagation, none of the parent elements will receive it, although there might be situations, when they should receive it.

A more elegant (and not that complex) way to solve this problem, is to always allow the event propagation by never calling stopPropagation() at all. Elements, which define their own events which shall not be automatically executed from a child element, can use the target and currentTarget properties to check from where the initial event comes and execute own event functions only in situations when it is wanted.

In following example, there are 3 nested DIVs. Clicking the lowest (blue) DIV shall propagate the onClick event up through the entire DOM structure but without calling the onClick event of the green DIV, which lies between:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            div {
                min-width: 100px;
                min-height: 50px;
                display: inline-block;
                padding: 2em;
            }
            #div1 { background: red; }
            #div2 { background: green; }
            #div3 { background: blue; }
            #info { display: block; white-space: pre; }
        </style>
        <script>
            window.addEventListener('load', function() {

                // #div1, #div3
                document.querySelector('#div1').addEventListener('click', function(e) {
                    if (e.target == e.currentTarget ||
                        e.target == document.querySelector('#div3')) {
                        document.querySelector('#info').textContent +=
                            'I am #1 or #3\n';
                    }
                });

                // #div2
                document.querySelector('#div2').addEventListener('click', function(e) {
                    if (e.currentTarget == e.target) {
                        document.querySelector('#info').textContent += 'I am #2\n';
                    }
                });
            });
        </script>
    </head>
    <body>
        <div id="div1">
            <div id="div2">
                <div id="div3"></div>
            </div>
        </div>
        <div id="info"></div>
    </body>
</html>

So calling stopPropagation() in onClick event function of DIV #3 is not necessary required to prevent firing the onClick event if DIV #2.

Also note, that the document structure is as follows:

document
  document.documentElement
    document.body
      ...

If an event propagation is not stopped, it will reach the document object. event.currentTarget will then be document, while event.target will be either document.documentElement, document.body or any sub element under the <body> element.

So considering you have following code:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <style>
            html {
                background: #009688;
            }
            body {
                background: #bbb;
            }
        </style>
    </head>
    <body>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
        <div>Hello world</div>
    </body>
</html>

Here is what it looks like and where the different parts of the document are: Simple page

Gray is the body area. Green is the actual document "element" (top most styleable part). And behind it, there is the invisible document object.

If you want to use event functions, which will be executed only for the elements directly under your finger / mouse cursor, you could use following code (example for onClick event):

elm.addEventListener('click', function(e) {
    if
    (
        (
            (e.currentTarget == document) &&
            (e.target == document.documentElement || e.target == document.body)
        )
        ||
        (e.currentTarget == e.target)
    )
    {
        // ...
    }
});

It works with document.documentElement, document.body or any element on the document without needing to call stopPropagation().

Sportswoman answered 28/10, 2017 at 4:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.