If you’ve read any of my posts you might have seen the pattern I like to use for using event delegation. That pattern, in its simplest form, looks like this:

$(document).ready({
   $('body').on('click','div',changeSomething);

   function changeSomething(e){
      // do something
   }
});

For me this is super useful – I can dynamically add elements to the DOM and not need to worry about attaching event listeners to specific UI elements. Well, as you can see I have the click event mapped to div elements. This works fine for something as simple as this:

<div>hello world</div>
<div>the sky is blue</div>

If I click on one of the above divs my changeSomething event will fire. If that’s what I wanted to happen then this is working perfectly. But what happens when we introduce more complicated layout with nested div‘s?

<div>
  <div>hello world</div>
  <div>the sky is blue</div>;
</div>

Each time I click on within either string of text **2** events are fired and my function will be fired twice. Consider this layout:

<div>
  <div>
    <p>hello world</p>
    <div class="button">I'm a button</div>
  </div>
  <div>
    <p>the sky is blue</p>
    <div class="button">I'm another button</div>
  </div>
</div>

If I clicked the “button” divs you will see that **3** events are fired for each click. Why would this be?

First, lets understand how event delegation works.

Event delegation relies on event bubbling. Event bubbling is the behavior that describes how events “bubble up” through the DOM. That is, they start at the DOM element where it was initiated and then travel up to that element’s parent, and then up to the parent’s parent, etc., until the event is stopped somehow (perhaps via the event.stopPropagation() method) or it reaches the top level DOM element.

Event bubbling is key to delegating events – notice that I bound the click event to the body – this means that my code is waiting for “click’s” to “bubble” their way up to the body. If they didn’t bubble through the DOM then the event listener I created would never “hear” the event.

While this explains why it is possible to delegate events it doesn’t explain why when using jQuery to create event listeners that we “hear” multiple events each time we click on a div one single time. This bit from the jQuery documentation provides the answer:


jQuery bubbles the event from the event target up to the element where the handler is attached (i.e., innermost to outermost element) and runs the handler for any elements along that path matching the selector.

Note that the docs says “…and runs the handler for any elements along that path matching the selector. Therein lies the answer. By definition jQuery will fire our callback handler for each element that matches the selector that I specified when I created the event listener. Because event bubbling means that the event will travel up though the DOM the event is therefore “hitting” other DOM elements that match the given selector. Each time this happens the callback handler is fired and the callback receives the very same event object.

Now that we understand what is happening we can look at our HTML and know that this structure:

<div> <!-- the event bubbles to here and the callback is fired a third time -->
  <div> <!-- the event bubbles to here and the callback is fired a second time -->
    <p>hello world</p>
    <div class="button">I'm a button</div> <!-- click here to fire the click event and the first callback -->
  </div>
</div>

…will cause our callback handler to fire 3 times from a single event instead of firing only once (see the in-code comments above).

How do we setup the listener to prevent this from happening? Well…. we could create a unique selector like .btn but I find that restrictive because it creates unnecessary JS hooks into our HTML (imagine all the selectors you would have, i.e., .btn .add .delete .menu .expand… what a mess!!!). In the end jQuery behaves the way that it behaves but we can easily write a small bit of code to prevent a single event from firing our callbacks multiple times.

Recall that each callback is passed the event object which will be the same for them all. As a result if you log those events to the console you will in fact see that they all have the same timestamp since, quite naturally, they are in fact identical. With this knowledge we can build a “throttling” function. Throttling is the term used to allow the first function call, but prevent subsequent calls.

Review the following (its very much simplified for this example):

   $(document).ready({

      var _fifo = []; // used by the throttler

      // throttle
      function _isOriginal(e){
        for (var i=0;i<_fifo.length;i++){
          if (_fifo[i] === e.timeStamp){
            return false;
          }
        }

        _fifo.push(e.timeStamp);
        if (_fifo.length > 20){
          _fifo.splice(0,1);
        }
        return true;
      }

      // create event listener
      $('body').on('click','div',changeSomething);

      // the callback for the delegated "click" event
      function changeSomething(e){
        // check the event to see if we have already seen it!
        if (!_isOriginal(e)){
            return; // if we ***have*** already seen the event then ignore it!
        }
        // do stuff in here
      }
   });

Pay attention to the _isOriginal function. It does the following:

  • Uses the _fifo array to store the 20 most recent timestamps (“20” is arbitrary, pretty sure 1 or 2 will work as well)
  • Compares the timestamp of the current event against those stored within the _fifo array
  • If there is a match it returns “false” (ignore the event)
  • If there is no match it returns “true” (new event!)
  • Timestamps are cycled in and out of the _fifo array at all times so that we will always have a timestamp from the most recent event and thus will always be able to compare and reject copies. The last/oldest timestamp is pruned (via the splice method ) from the _fifo array once its length hits 20 while the newest is added to the front (read about FIFO stacks)

There you go, a simple and effective “callback throttler”. As always, if you understand how things work you may just find that you don’t need to go in search of a plugin to solve your problem. A little “under-the-hood” knowledge goes a long way 🙂