Efficient Event Targeting with JS

Today I would like to talk a bit about setting up events on DOM using jQuery, and how one should try to avoid setting up huge numbers of unnecessary events on a single document. When setting up events, you are allocating little bits of memory to store procedures and logic into that will be ran upon whatever event type you apply, is indeed performed.

If when say faced with a huge list of records (let’s say 3 or 4 hundred records), and each record has “additional details” that requires a user toggle open and closed, then one can see that the number of events can add up fast.

Let’s take a look at a snip of markup from this list of records I mentioned above:

<ul class="records">
    ...
    <li>
        Basic Details <span class="toggle-details">See More</span>
        <section class="additional-details">
            <h2>More Details</h2>
            <p>Here is more details about this record.</p>
        </section>
    </li>
    ...
</ul>

So, assuming we have about 3 or 4 hundred list items (or rather records on our DOM), we would need to target each records span tag that are classed with “toggle-details”. Once we select each element, we then must assign a click event to each element that will decide to hide, or display a sibling section element.

One way to write that logic is as follows:

Vanilla JS version

var toggles, i;
toggles = document.querySelectorAll('.records .toggle-details');
i = toggles;
while(i--){
    var details = toggles[i].parentNode.querySelector('.additional-details');
    details.onclick = function(){
        this.style.display = (this.style.display == 'none') ? '' : 'none';
    };
}

jQuery version

$('.records .toggle-details').each(function(){
    $(this).on('click', function(){
        $(this).parent('li').find('.additional-details').toggle();
    });
});

This will target each .toggle-details span tag, and bind a click event to each element. Once any of the elements are clicked, the logic asks for that clicked element to look to its parent wrapper, then search for a child called. additional-details to decide if it should be hidden or displayed.

The problem with our above code, is that we are asking our document to reserve 3 or 4 hundred events in memory for each span tag that has a class of “.toggle-details”. YIKES!

A much more efficient way to do this would be to point to a single parent of said elements, then use a single event listening instead:

Vanilla JS version

var records = document.querySelectorAll('.records');
records.addEventListener('click', function(e){
    var target = e.target;
    var parent = target.parentNode;
    if(parent.className.match('.toggle-details')){
      var details = parent.querySelector('.additional-details');
      details.style.display = (details.style.display == 'none') ? '' : 'none';
    }
});
details.onclick = function(){
    this.style.display = (this.style.display == 'none') ? '' : 'none';
};

jQuery version

$('.records').bind('click',function(e){
    var target = $(e.target);
    if(target.is('.toggle-details')){
        target.parent('li').find('.additional-details').toggle();
    }
});

Here we not applying hundreds of events, but rather just one event listener to a single parent wrapper. Within the event listener we then utilizing the native event prop (e) and asking the event what element (e.target) was interacted with under said parent.

The “event.target” prop is very powerful and helpful prop that allows us to get the first element of a any bubbled up elements out of a event and event listeners. This means that instead of binding hundreds of events to elements across the page, we now only have a single event listener with a single condition to meet elements for any logic.

Kind of awesome right; but wait it gets better! Why stop at an immediate parent?
In theory, if a person is going setup say a single click event, why not setup one the body of the document?

Vanilla JS version

document.body.addEventListener('click', function(e){
    var target = e.target;
    if(...){ .... }
});

jQuery version

$('body').bind('click',function(e){
    var target = $(e.target);
    if(....){ .... }
});

With body listener, you never have to worry about losing events/logic when DOM is added, altered or move cause it’s all rolled up into document.body that never changes. If you do decide to start wrapping up your event logic into the body under a single event, it is helpful to know that you want to reserve such logic as “e.preventDefault()” in side the meeting conditions, like so:

Vanilla JS version

document.body.addEventListener('click', function(e){
    var target = e.target;
    if(target.className.match('.some-anchor-tag')){
        ....
        e.preventDefault();
    }
});

jQuery version

$('body').bind('click',function(e){
    var target = $(e.target);
    if(target.is('.some-anchor-tag')){
        ...
        e.preventDefault();
    }
});

Notice the e.preventDefault(); lives in the met condition? This is important, cause if you have it outside the condition, all links on the page will stop working. So, when using single element event binding and event targets, you need to think of your conditions as actual selectors, and keep your logic in them as you would any anonymous function.

Some common helpful variables one might want to setup if you are using a body to wrap your event logic into, are:

  • var target = e.target = Used to find the element with in our event that has been interacted with.
  • var parent = target.parentNode; = Points the the immediate parent wrapper of our found target element.

So to conclude; with the single binding of an event listener and the e.target prop, we no longer need to setup hundreds of events to elements anymore. We also no longer need to worry about events being lost when elements get moved, or altered in our document if using this method high enough in your DOM.

Thanks, and enjoy!
Devin R. Olsen

Devin R. Olsen

Devin R. Olsen

Located in Portland Oregon. I like to teach, share and dabble deep into the digital dark arts of web and game development.

More Posts

Follow Me:TwitterFacebookGoogle Plus