Javascript - Listing active event listeners on a Web page

Introduction

When developing a Web site, event listeners are implemented, by third party libraries or by your own.

An example, a listener on the event dragstart for a div element

:
dv = document.getElementById('col-left');

dv.addEventListener('dragstart', function(e) {
    dv.style.opacity=0.6;
    e.dataTransfer.dropEffect="move";
});

The listener above can also be defined differently using the associated event attribute ondragstart :

dv = document.getElementById('col-left');
          
dv.ondragstart =
  function(e) {
        dv.style.opacity=0.6;
        e.dataTransfer.dropEffect="move";
  };

Or directly in the HTML code if not defined by a Javascript function :

<div id="col-left"
ondragstart="function(e) { this.style.opacity=0.6; e.dataTransfer.dropEffect='move';">

Basically, 2 ways when defining events, just need to add the prefix on using the event attribute :


element.addEventListener('click', function() { … }); element.onclick = function() { … };
element.addEventListener('load', function() { … }); element.onload = function() { … };

In some circumstances, when starting performance improvements tasks or when debugging behavior issues in event listeners due to a third party library, we may need to get an overview of all event listeners. How to get the full list : the events defined with addEventListener and the ones defined with the corresponding attribute ?

It’s not so trivial.

Listing the events defined with the event attribute

We can find useful scripts on the web

function listAllEventListeners() {
  const allElements = Array.prototype.slice.call(document.querySelectorAll('*'));
  allElements.push(document);
  allElements.push(window);
  
  const types = [];
  
  for (let ev in window) {
    if (/^on/.test(ev)) types[types.length] = ev;
  }

  let elements = [];
  for (let i = 0; i < allElements.length; i++) {
    const currentElement = allElements[i];
    for (let j = 0; j < types.length; j++) {
      if (typeof currentElement[types[j]] === 'function') {
        elements.push({
          "node": currentElement,
          "type": types[j],
          "func": currentElement[types[j]].toString(),
        });
      }
    }
  }

  return elements.sort(function(a,b) {
    return a.type.localeCompare(b.type);
  });
}

Great scripts. In the above script, the global object window is also added as we want to get also listeners defined for this object, especially scrolling events for debugging purposes.

> console.table(listAllEventListeners())
Results events defined with the event attribute

First we are happy but we immediately notice some event listeners we have developed are missing.

Which events and why ? After investigations : all events defined with the method addEventListener are missing, they are not stored in the object’s event attributes on<event> (onclick, onload, onfocus…).

Listing the events added with the method addEventListener

No native method exists yet in the DOM specifications to retrieve the events defined through the method addEventListener.

getEventListeners in developer tools

Browsers developer tools, Chrome developer tools for example, offer in the console the method getEventListeners()

Chrome Dev Tools - getEventListeners()

But this method is only available in developer tools.

Overriding addEventListener prototype

If we want this method to be available in scripts, we have to override the addEventListener prototype.

The override consists in adding an object eventListenerList that will store added event listeners. The method that will retrieve the event listeners will return this object.

For example, for the interface Window, the prototype is modified as follows :

Window.prototype._addEventListener = Window.prototype.addEventListener;

Window.prototype.addEventListener = function(a, b, c) {
   if (c==undefined) c=false;
   this._addEventListener(a,b,c);
   if (! this.eventListenerList) this.eventListenerList = {};
   if (! this.eventListenerList[a]) this.eventListenerList[a] = [];
   this.eventListenerList[a].push({listener:b,options:c});  
};

This code should be applied also for the interfaces Document and Element.

To avoid duplicate code, the prototype modification can be applied once in the interface EventTarget.

EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them.

Element, Document, and Window are the most common event targets, but other objects can be event targets, too. For example XMLHttpRequest, AudioNode, AudioContext, and others.

EventTarget.prototype._addEventListener = EventTarget.prototype.addEventListener;

EventTarget.prototype.addEventListener = function(a, b, c) {
   if (c==undefined) c=false;
   this._addEventListener(a,b,c);
   if (! this.eventListenerList) this.eventListenerList = {};
   if (! this.eventListenerList[a]) this.eventListenerList[a] = [];
   this.eventListenerList[a].push({listener:b,options:c});  
};

Implement this piece of code as soon as possible in the page architecture and loading mechanism. Here, it is inserted at the beginning of the script afwk.js, first javascript script loaded in the page.

The method _getEventListeners

Now, each type of object (window, document, element) exposes the object eventListenerList containing the added listeners. The method _getEventListeners returning the object eventListenerList is created.

EventTarget.prototype._getEventListeners = function(a) {
   if (! this.eventListenerList) this.eventListenerList = {};
   if (a==undefined)  { return this.eventListenerList; }
   return this.eventListenerList[a];
};

It is ready :

  function _showEvents(events) {
    for (let evt of Object.keys(events)) {
        console.log(evt + " ----------------> " + events[evt].length);
        for (let i=0; i < events[evt].length; i++) {
          console.log(events[evt][i].listener.toString());
        }
    }
  };
  
  console.log('Window Events====================');
  wevents = window._getEventListeners();
  _showEvents(wevents);

  console.log('Div js-toc-wrap Events===========');
  dv = document.getElementsByClassName('js-toc-wrap')[0];
  dvevents = dv._getEventListeners();
  _showEvents(dvevents);

Window Events====================
resize ----------------> 4
function() { resize_progressread(pbar); }
function(){var t=[];r("pre[data-line]").forEach(function(e){t.push(s(e))}),t.forEach(i)}
function(){Array.prototype.forEach.call(document.querySelectorAll("pre."+l),m)}
function(){var j=new Date,k=b-(j-h);return d=this,e=arguments,k<=0?(clearTimeout(f),f=null,h=j,g=a.apply(d,e)):f||(f=setTimeout(i,k+c)),g}

beforeprint ----------------> 1
function() {
	document.getElementById('menu-checkbox-toc').checked=true;
	Afwk.toc.div.style.position = 'static';
	Afwk.toc.div.className = "js-toc-wrap";
	Afwk.toc.stuck = false;
}
…
Div js-toc-wrap Events===========

dragend ----------------> 1
function(e) {						
  Afwk.toc.div.style.opacity=1;
  // FF workaround, clientY not available
  posY = (e.clientY == 0) ? e.screenY : e.clientY;
  Afwk.toc.div.style.top = posY + "px";
  Afwk.toc.floating.top = posY + "px";
  dwrap = document.getElementById('wrap');
  dwrap.removeAttribute('ondragover');
  event.preventDefault();
  return false;
}
  • Functions for an event are ordered by timestamp creation.
  • As expected, functions defined in event attributes are not retrieved.

We get the functions source code, that’s a good starting point for optimization and debugging purposes. It is possible to get also the function location ([[FunctionLocation]]) but only in the browsers developer tools using console.log on the event :


  for (let evt of Object.keys(events)) {
      console.log(evt + " ----------------> " + events[evt].length);
      for (let i=0; i < events[evt].length; i++) {
        …
        console.log(events[evt][i]);
      }
  }
Chrome Developer Tools | [[FunctionLocation]]

Could not find a way to retrieve porgramatically [[FunctionLocation]].

Events list - Merged and final version listAllEventListeners

After implementing the method _getEventListeners, the merged version will retrieve both the events defined in attributes and the ones defined with addEventListener. The list is now complete, that’s better for debugging.

function listAllEventListeners() {
  const allElements = Array.prototype.slice.call(document.querySelectorAll('*'));
  allElements.push(document);
  allElements.push(window);
  
  const types = [];
  
  for (let ev in window) {
   if (/^on/.test(ev)) types[types.length] = ev;
  }
  
  let elements = [];
  for (let i = 0; i < allElements.length; i++) {
    const currentElement = allElements[i];
    
    // Events defined in attributes
    for (let j = 0; j < types.length; j++) {
      
      if (typeof currentElement[types[j]] === 'function') {
        elements.push({
          "node": currentElement,
          "type": types[j],
          "func": currentElement[types[j]].toString(),
        });
      }
    }
    
    // Events defined with addEventListener
    if (typeof currentElement._getEventListeners === 'function') {
      evts = currentElement._getEventListeners();
      if (Object.keys(evts).length >0) {
        for (let evt of Object.keys(evts)) {
          for (k=0; k < evts[evt].length; k++) {
            elements.push({
              "node": currentElement,
              "type": evt,
              "func": evts[evt][k].listener.toString()
            });		
          }
        }
      }
    }
  }

  return elements.sort();
}

The sort option used is slightly different than previously when getting the events defined in event attributes. With the minimally sort return elements.sort(); it is guaranteed that events created with the method addEventListener are sorted by timestamp creation for an event type : useful when debugging because functions are fired in this order.

> console.table(listAllEventListeners);
Chrome Developer Tools | Full Events list

No need to add a column specifying if the event is defined in an attribute event or through addEventListener. When the event name starts with the prefix on, it is an event attribute.

removeEventListener and list update

Removing an event defined by its attribute is done with the method removeAttribute :

dv = document.getElementById('col-left');
dv.removeAttribute('ondragenter');

In such a case, no problem : the list returned by listAllEventListeners is dynamically updated.

What about the events removed with removeEventListener, examples :

document.removeEventListener('scroll', Afwk.lazyLoad);
window.removeEventListener('resize', Afwk.lazyLoad);
window.removeEventListener('orientationchange', Afwk.lazyLoad);
window.removeEventListener('beforeprint', Afwk.forceLazyload);

The list returned by the method _getEventListeners is not updated.

The removeEventListener method prototype is overriden as we did previously for the method addEventListener by adding code to remove the event in the object eventListenerList. Piece of code to be added as soon as possible in the load page mechanism.

EventTarget.prototype._removeEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function(a, b ,c) {
   if (c==undefined) c=false;
   this._removeEventListener(a,b,c);
   if (! this.eventListenerList) this.eventListenerList = {};
   if (! this.eventListenerList[a]) this.eventListenerList[a] = [];

   for(let i=0; i < this.eventListenerList[a].length; i++){
      if(this.eventListenerList[a][i].listener==b, this.eventListenerList[a][i].options==c){
          this.eventListenerList[a].splice(i, 1);
          break;
      }
   }
   if(this.eventListenerList[a].length==0) delete this.eventListenerList[a];
};

Conclusion

For debugging purposes, performance diagnosis : listing events is not so trivial depending on how they are defined (event attributes or addEventListener).

Further more, it raises one question : during development phase, event attributes or addEventListener / removeEventListener, or both ? A coding standards to be defined depending on the project requirements and complexity.

A little disappointment, maybe someone could give the solution : it seems there is no easy solution to retrieve programmatically the property [[FunctionLocation]].