Javascript - Lister les event listeners actifs sur une page Web

Logo

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. Dans cet article est décrite la fonction globale listAllEventListeners qui liste tous les événements, peu importe la manière dont l’événement a été créé. La fonction listAllEventListeners est active dans cette page, dans la console de développement du navigateur :

console.table(listAllEventListeners());

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]].