Career Corner Blog Posts
Blog posts are a great way for SAP, customers, and partners to share advice, insights into career trends, new opportunities, and personal success stories.
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

Introduction

JavaScript doesn't have a comforting garbage collection mechanism as other languages, such as Java, C# etc., do. Best coding practices always advice freeing up memory by deleting objects which no longer are needed to enhance garbage collection. More than dealing voraciously with objects, we deal with event handlers attached to those objects. Numerous (and frequent) events could be monitored for an object and hence we've an avalanche of event handlers. Worst case scenarios end up with memory leaks leading to performance plummeting or application failure. All we need is to make sincere efforts towards meticulously creating/managing objects, event handlers etc. This article will focus on "Event Propagation" in JavaScript, then turning into Event Delegation, a design pattern to avoid creating numerous event handlers thereby boosting performance.

Event Propagation

We web developers would have definitely seen such code:

function someEventHandler(event){

event.stopPropagation();

event.preventDefault();

//custom implementation goes here...

}

While event.preventDefault() prevents the default action which would have been triggered otherwise, the focus for this article will be on event.stopPropagation().

As it says, it's stopping the propagation of an event. So how do events actually propagate? There's some history to it. Let's say we have a situation wherein our HTML looks like this:

<div id='elementOne'>

<div id='elementTwo'></div>

</div>

There's a nested <div> element and say, both the <div> elements have a onclick event handler attached to them. Now, when elementTwo is clicked, the click event handlers of both the <div> elements get fired. The question is, which one fires first? There are two disparate (browser) theories to answer this question:

  1. Event Capturing - This school of thought says that events always move from top to bottom i.e. in our case, event listener of elementOne fires first followed by that of  elementTwo.
  2. Event Bubbling - this theory says that events always move from bottom to top, i.e. event listener of elementTwo fires first and then that of elementOne.

    

The W3C spec however takes a neutral stand on this and states that all events are Captured first till they reach the target element (element which originated the event) at which they Bubble up again. However, when the event reaches the target element, it's no longer capturing/bubbling. Since it's performing neither, event handlers are executed in the order in which they are registered.

The addEventListener method

To add an event listener to any element, we do:

document.getElementById('elementOne').addEventListener('click', eventListenerCallback, flag);

It's in the third variable flag which decides whether we choose to use event capture or event bubble. If flag is set to

  1. true - then we create the handler for the capture event
  2. false - then we create the handler for the  bubble event

    

Since the behavior of event propagation is browser dependent, special care is needed to ensure consistent cross-browser behavior. Libraries like jQuery make such efforts to maintain consistency.

Why is it important?

Consider the situation wherein your application shows substantial amount of data (lets say in tables). You do not know what could be the size of the tables in terms of the number of rows i.e. the tables are built dynamically. However, you do want that once a table row is clicked, you want to display some information related to the clicked row. This situation is very common indeed. So, our JavaScript would handle this click event:

<table id='myTable'>

<tr>….</tr>

<tr>….</tr>

<tr>….</tr>

<tr>….</tr>

</table>

var table = document.getElementById('myTable');

var rows = table.getElementsByTagName('tr');

for(var i=0;i<rows.length;i++){

rows[i].addEventListener('click', function(){

//custom implementation goes here...

}, false);

}

jQuery implementation:

$('#myTable tr').on('click', function(evt){

//custom implementation goes here...

});

p.s.:  events always bubble in jQuery

So far, there's nothing of interest. But if your application generates thousands of rows, then for each row we attach an event handler. So, for example if we have 500 rows then we've 500 click event handlers, if we have 1000 rows then we've 1000 click event handlers. Further, if you application is quite interactive, thereby adding/removing table rows, then we should know that adding/removing table rows is quite an expensive task and not to forget the garbage collection efficiency in JavaScript which compounds the agony.

There's nothing WRONG in this implementation. It's just that we can do better than this. Thanks to event bubble propagation, we can make amends to our implementation. Instead of attaching event handlers to our rows <tr>, we could attach the event handler to the parent <table> element. Then we could monitor as the event bubbles up from the <tr> up to the <table>. So, we shall change our JavaScript a bit:

var table = document.getElementById('myTable');

table.addEventListener('click', function(event){

var targetElement= event.target;

while(_targetElement !== table){

if(_targetElement.nodeName === "TR" && _targetElement.parentNode && _targetElement.parentNode.nodeName === "TBODY"){

//custom logic goes here…

console.log("row number "+_targetElement.rowIndex+" was clicked");

}

_targetElement = _targetElement.parentNode;

}

}, false);

Before dissecting the code, lets understand what the flow looks like:

  1. User clicks on a row cell <td> element or on <table> element. This event bubbles up from the <td> to the <table> following which we check the target element which fired this event. In this case, the target element is a <td> element.
  2. Now, we can traverse the DOM tree upwards to find which row was clicked, by identifying them based on nodeName property and then perform our custom operation.

    

Smooth right ! This process is called Event Delegation. We've delegated the click event on a <tr> to a <table> element and using event bubble mechanism, we ensure that the correct source is the caller of the callback.

jQuery implementation:

$('#myTable').on('click', 'tr', function(){

//custom implementation goes here...

});

It's even easier in jQuery. It automatically takes care of the extra effort of matching target elements and then allowing our custom code to be executed (one of the many reasons I admire jQuery so much).

What's the gain?

Did you  notice that because of event delegation, we have created just ONE event handler as opposed to thousands in the absence of delegation? Our table could still have thousands of rows inside, however their clicks would still be taken care of with just one event handler attached on the <table> element. This gives considerable performance boost if our application is heavily data driven (which most applications are). Moreover, we also show some mercy to the garbage collection rigmarole thereby making our application still efficient.

Gotchas

There are two important properties of the event object passed to the event handler:

  1. target - the target element which originated the event
  2. currentTarget - current element in the event flow

Now lets say there's an event handler and an event was fired, if the currentTarget and the target elements are same, then we are at the target element in the event flow and thus the event does neither capture nor bubble. And in this case, if the element has both callbacks registered (for capture and bubble), then the order of execution of the respective callbacks will be in the order of their declaration. For e.g.:


<table>

<tr><td>hello</td></tr>

</table>

If I have click event handlers on the <table> and the <td> element (cell):

/** bubble event handler */

table.addEventListener('click', function(evt){

console.info("Triggered >> bubble table");

}, false);

/** capture event handler */

table.addEventListener('click', function(evt){

console.info("Triggered >> capture table");

}, true);

/** bubble event handler */

cell.addEventListener('click', function(evt){

console.info("Triggered >> bubble cell");

}, false);

/** capture event handler */

cell.addEventListener('click', function(evt){

console.info("Triggered >> capture cell");

}, true);

What would be the console output if I click on a cell? As per the W3C spec, the output order will be:

Triggered >> capture table

Triggered >> bubble cell

Triggered >> capture cell

Triggered >> bubble table


This is because, the flow is Capture first till you reach the target element at which there's no bubbling/capturing. Event handlers here are executed in the same order as they are defined. Then we bubble back again to the parent. So, we capture the <table> click first, then we reach the <td> (which is the target element), so ideally the output should have been capture cell and then bubble cell, but here the output depends on the order in which the event handlers are declared. Then finally we again start with bubbling to the <table> element.

Conclusion

Event Delegation is one of the very popular JavaScript design patterns. It solely depends on a single container encompassing massive content within, in which case defining separate event handlers for all child content could be redundant and inefficient. Click event though could be infrequent, but mousemove or scroll events are very much frequent and such events demand optimized processing. Further, one important thing to note is that we should not delegate an event to the top of document tree because in these cases we end up filtering out all selectors and move up till the top of the document. We should mostly restrict our delegation to elements close to the target element.