Javascript - Lister les event listeners actifs sur une page Web

Introduction

Lors du développement d'un site Web, des écouteurs d'événements, appelés event listeners par abus de langage en français, sont implémentés soit par des librairies tierces, soit par soi même.

Un exemple, un écouteur sur l'événement dragstart pour un élément div :

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

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

Le listener ci-dessus peut être défini différemment en utilisant l'attribut d'événement associé ondragstart :

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

ou directement dans le code HTML si l'événement n'est pas créé par une fonction Javascript :

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

Pour résumer, 2 manières de définir des événements, il faut juste ajouter le préfixe on quand l'attribut d'événement est utilisé :


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

Dans certaines circonstances, lors du démarrage des tâches d'améliorations des performances ou lors du debug de problèmes de comportement avec les event listeners à cause d'une librairie tierce, il s'avère utile d'obtenir une cartographie de tous les event listeners. Comment obtenir cette liste complète : les événements définis avec addEventListener et ceux définis avec l'attribut correspondant ?

Ce n'est pas aussi trivial que cela pourrait l'être.

Lister les événements définis avec l'attribut événement

On trouve sur le Web des scripts déjà prêts pour lister les événements.

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);
  });
}

Très bons scripts. Dans le script ci-dessus, l'objet global window est également ajouté car on veut dans le cas pratique ici surtout les listeners définis pour cet objet, notamment les événements de déroulement de la page (scrolling events) à des fins de debug.

> console.table(listAllEventListeners())
Résultats pour les événements définis avec l'attribut événement

Au début on est content, mais on constate très rapidement que des événements que l'on a développés manquent à l'appel dans le résultat.

Quels événements et pourquoi ? Après investigations : tous les événements définis avec la méthode addEventListener sont absents, ils ne sont pas stockés dans les attributs d'événements on<event> de l'objet (onclick, onload, onfocus …).

Lister les événements ajoutés avec la méthode addEventListener

Aucune méthode native n'existe encore dans les spécifications DOM (Document Object Model) afin de lister les événements définis via la méthode addEventListener.

getEventListeners dans les outils de développement

Les outils de développement des navigateurs, Chrome par exemple, offrent dans leur console la méthode getEventListeners()

Chrome Outils de développement - getEventListeners()

Mais cette méthode est disponible uniquement dans les outils de développement.

Surcharge du prototype addEventListener

Pour que cette méthode soit disponible par scripts, il faut surcharger le prototype de la méthode addEventListener.

La surcharge consiste à ajouter un objet eventListenerList qui stockera tous les event listeners ajoutés. La méthode qui extraira les event listeners retournera cet objet.

Par exemple, pour l'interface Window, le prototype sera modifié ainsi :

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});  
};

Ce code devrait être appliqué également pour les interfaces Document et Element.

Afin d'éviter la duplication de code, la modification peut être appliquée en une seule fois pour l'interface EventTarget.

EventTarget est une interface du DOM implémentée par les objets qui peuvent recevoir des événements et avoir des écouteurs (listeners) pour ceux-ci.

Element, Document, et Window sont les cibles d'événements (event targets) les plus communs, mais d'autres objets peuvent être des cibles d'événements également. Par exemple XMLHttpRequest, AudioNode, AudioContext, et d'autres.

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});  
};

Implémenter ce morceau de code le plus tôt possible dans l'architecture de la page et son mécanisme de chargement. Ici, il est inséré au début du script afwk.js, premier script javascript chargé dans la page.

La méthode _getEventListeners

Maintenant, chaque type d'objet (window, document, element) expose l'objet eventListenerList contenant les listeners ajoutés. La méthode _getEventListeners retournant l'objet eventListenerList est créée.

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;
}
  • Les fonctions pour un événement sont triées par timestamp de création.
  • Comme attendu, les fonctions définies par les attributs d'événement ne sont pas extraites.

On a le code source des fonctions, c'est un bon point de départ à des fins de debug et d'optimisation. Il est également possible d'obtenir la localisation de la fonction ([[FunctionLocation]]) mais uniquement dans les outils développeur du navigateur en utilisant console.log sur l'événement :


  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 Outils de développement | [[FunctionLocation]]

Impossible de trouver une façon simple d'extraire par programmation [[FunctionLocation]].

Liste des événements - Version finale fusionée listAllEventListeners

Après avoir implémenté la méthode _getEventListeners, la version fusionnée extrait à la fois les événements définis par les attributs et ceux définis avec addEventListener. La liste est maintenant complète, c'est bien mieux pour déboguer.

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();
}

L'option de tri utilisée est alors légérement différente que précédemment lors de l'extraction des événements définis par des attributs. Avec le tri minimaliste return elements.sort(); on garantit que les événements créés avec la méthode addEventListener sont triés par timestamp de création pour un type d'événement : très utile lors du debug car les fonctions sont déclenchées dans cet ordre.

> console.table(listAllEventListeners);
Chrome Outils de développement | Liste complète des événements

Pas besoin d'ajouter une colonne indiquant si l'événement est défini dans un attribut ou avec addEventListener. Lorsque l'événement commence par le préfixe on, il s'agit d'un attribut d'événement.

removeEventListener et mise à jour de la liste

Supprimer un événement défini par son attribut est réalisé avec la méthode removeAttribute :

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

Dans un tel cas, aucun problème : la liste retournée par listAllEventListeners est dynamiquement mise à jour.

Qu'en est-il des événements supprimés avec removeEventListener, exemples :

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

La liste retournée par la méthode _getEventListeners n'est alors plus à jour.

Le prototype de la méthode removeEventListener est surchargé comme cela a été fait précédemment pour la méthode addEventListener pour y ajouter le code qui supprime également l'événement dans l'objet eventListenerList. Morceau de code à ajouter le plus tôt possible dans le mécanisme de chargement de la page.

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

À des fins de debug ou de diagnostics de performances : lister les événements n'est pas si trivial selon comment ils sont définis (attributs d'événements ou addEventListener).

De plus, cela soulève une question : durant la phase de développement, attributs d'événement ou addEventListener / removeEventListener, ou les deux à la fois ? Une norme de codage à définir en fonction des besoins et de la complexité du projet.

Une petite déception lors de cette étude, peut-être quelqu'un donnera la solution dans un commentaire : il semble qu'il n'y ait pas de solution simple pour obtenir par programmation la propriété [[FunctionLocation]].