Debugging Asynchronous JavaScript with Chrome DevTools

HTML5 Rocks

Introduction

Parmi les fonctionnalités puissantes de JavaScript qui contribuent à en faire un langage unique, on trouve sa capacité à travailler de façon asynchrone à l’aide de fonctions de rappel. Utiliser des fonctions de rappel nous permet d’écrire du code piloté par événements, mais engendre aussi un débogage à s’arracher les cheveux, car JavaScript ne s’exécute alors pas de façon linéaire.

Heureusement, dans les Chrome DevTools, nous avons désormais la possibilité de voir la pile d’appels entière, rappels asynchrones compris !

Une bande-annonce rapide des piles d’appels asynchrones
Une petite bande-annonce rapide des piles d’appels asynchrones

(On analysera le déroulé de cette démo dans un instant.)

Une fois que vous aurez activé les piles d’appels asynchrones dans les DevTools, vous pourrez explorer l’état de votre appli web à des moments distincts dans le temps. Parcourez la pile d’appels complète pour y trouver des gestionnaires d’événements, setInterval, setTimeout, XMLHttpRequest, des promesses, requestAnimationFrame, des MutationObservers, et plus encore.

Lorsque vous parcourez la pile, vous pouvez analyser les valeurs de n’importe quelle variable au moment précis de l’exécution qui correspond à l’entrée de pile sélectionnée. C’est comme avoir une machine à remonter le temps pour des expressions espions !

Activons cette fonctionnalité et examinons ensemble quelques scenarii.

Activer le débogage asynchrone dans Chrome

Pour essayer cette nouvelle possibilité, activez-la dans Chrome. Allez dans le panneau Sources d’un Chrome récent (stable à partir de mars 2014, Canary depuis décembre 2013).

À côté du paneau Call Stack sur la droite (ou en bas, si vous êtes sur un espace étroit), vous trouverez une nouvelle case à cocher « Async ». Cochez-la pour activer le débogage asynchrone (et vous ne voudrez sans doute plus jamais la décocher).

(Dés)activer le débogage asynchrone

Capturer des rappels de timer et des réponses Ajax

Vous avez probablement déjà vu ce type de message dans Gmail par le passé :

Gmail qui ré-essaie d’envoyer un e-mail

Si l’envoi de la requête rencontre un problème (que ce soit côté serveur ou en raison de soucis de connectivité du côté client), Gmail ré-essaie automatiquement d’envoyer après un court délai.

Pour voir en quoi les piles d’appels asynchrones peuvent nous aider à analyser les rappels de timer et les réponses Ajax, j’ai recréé un faux Gmail à titre d’illustration. Le code JavaScript complet peut être trouvé via ce lien, mais le flux de contrôle est comme suit :

Diagramme de flux du faux Gmail d’illustration
Dans ce diagrame, les méthodes signalées en bleu plus foncé sont des candidats de choix pour notre nouvelle fonctionnalité DevTools, car ils déclenchent un traitement asynchrone.

Si on regardait la pile d’appels de versions précédentes des DevTools, un point d’arrêt dans postOnFail() nous donnerait trop peu d’informations sur le contexte de son appel. Mais regardez la différence lorsque les piles d’appels asynchrones sont activées :

Avant
Point d’arrêt dans notre faux Gmail sans les piles d’appels asynchrones
Le panneau Call Stack sans Async activé.

On voit que postOnFail() vient d’un rappel Ajax, mais aucune info supplémentaire.

Après
Point d’arrêt dans notre faux Gmail avec les piles d’appels asynchrones
Le panneau Call Stack avec Async activé.

On voit que notre appel XHR est parti de submitHandler(). Sympa !

Avec les piles d’appels asynchrones activées, il est facile de déterminer si la requête vient de submitHandler() (lorsqu’on a cliqué sur le bouton d’envoi) ou de retrySubmit() (qui survient après un délai par setTimeout()) :

submitHandler()
Point d’arrêt dans notre faux Gmail en situation nominale
retrySubmit()
Point d’arrêt dans notre faux Gmail en situation de ré-essai d’envoi

Expressions espions asynchrones

Lorsque vous parcourez votre pile d’appels complète, vos expressions espions se mettent aussi à jour pour refléter leur état à l’instant correspondant !

Un exemple d’expressions espions avec des piles d’appels asynchrones

Évaluer du code dans des portées passées

Non seulement vous pouvez simplement surveiller des expressions, mais vous pouvez interagir avec le code de portées passées directement dans le panneau Console des DevTools.

Imaginez que vous êtes Dr. Who et que vous ayez besoin d’aide pour comparer l’horloge d’avant que vous montiez à bord du Tardis avec « maintenant ». Depuis la console des DevTools, vous pouvez facilement évaluer, stocker, et faire des calculs avec des valeurs issues de différents points d’exécution.

Un exemple d’utilisation de la console avec des piles d’appels asynchrones
Utilisez la console JavaScript conjointement aux piles d’appels asynchrones pour déboguer votre code. La démo ci-dessus est disponible ici.

En restant dans les DevTools pour manipuler vos expressions, vous économiserez du temps en évitant de basculer dans votre code source, faire des modifications, puis rafraîchir votre navigateur.

Dérouler les résolutions de chaînes de promesses

Si vous trouviez que le flux du faux Gmail juste avant était difficile à déboguer sans les piles d’appels asynchrones, imaginez-vous à quel point des flux asynchrones plus complexes, tels que des chaînes de promesses, sont difficiles à déboguer sans aide ? Revoyons ensemble le dernier exemple du didacticiel de Jake Archibald sur les promesses en JavaScript.

Diagramme de flux des Promesses en JavaScript.

Voici une petite animation illustrant le parcours de la pile d’appels de l’exemple async-best-example.html de Jake.

Avant
Parcours de la résolution de promesses sans les piles d’appels asynchrones
Le panneau Call Stack sans Async activé.

Remarquez que le panneau Call Stack ne donne que très peu d’infos lorsqu’on essaie de déboguer nos promesses.

Après
Parcours de la résolution de promesses avec les piles d’appels asynchrones
Le panneau Call Stack avec Async activé.

Wow! Such promises. Much callbacks.

Apprenez-en davantage sur vos animations web

Fouillons plus loin dans les archives de HTML5Rocks. Vous vous souvenez du didacticiel Leaner, Meaner, Faster Animations with requestAnimationFrame de Paul Lewis ?

Ouvrez la démo requestAnimationFrame et ajoutez un point d’arrêt au début de la méthode update() (vers la ligne 874) de post.html. Avec les piles d’appels asynchrones, nous récupérons beaucoup plus d’informations sur requestAnimationFrame, et gagnons notamment la possibilité de revenir en arrière jusqu’au tout premier rappel d’événement de défilement.

Avant
Point d’arrêt sur requestAnimationFrame sans les piles d’appels asynchrones
Le panneau Call Stack sans Async activé.
Après
Point d’arrêt sur requestAnimationFrame avec les piles d’appels asynchrones
Et avec Async activé.

Suivez les mises à jour du DOM en utilisant MutationObserver

MutationObserver nous permet de surveiller les modifications du DOM. Dans cet exemple simple, lorsque vous cliquez sur le bouton, un nouveau nœud DOM est ajouté au <div class="rows"></div>.

Ajoutez un point d’arrêt dans la méthode nodeAdded() (ligne 31) de demo.html. Avec les piles d’appels asynchrones activées, vous pouvez désormais parcourir la pile d’appel antérieure à addNode() jusqu’à l’événement click initial.

Avant
Point d’arrêt sur un MutationObserver sans les piles d’appels asynchrones
Le panneau Call Stack sans Async activé.
Après
Point d’arrêt sur un MutationObserver avec les piles d’appels asynchrones
Et avec Async activé.

Astuces pour le débogage JavaScript à l’aide des piles d’appels asynchrones

Nommez vos fonctions

Si vous avez tendance à utiliser des fonctions de rappel anonymes, vous voudrez sans doute leur donner un nom pour faciliter la lecture de votre pile d’appel.

Par exemple, prenez une fonction anonyme comme celle-ci :

window.addEventListener('load', function(){
  // faire quelque chose
});

Et donnez-lui plutôt un nom, genre windowLoaded() :

window.addEventListener('load', function windowLoaded(){
  // faire quelque chose
});

Lorsque l’événement load se déclenche, il apparaît dans la pile d’appels des DevTools avec le bon nom de fonction, au lieu de l’inutile « (anonymous function) ». Il est alors bien plus facile de comprendre d’un coup d’œil ce qui se passe dans votre pile d’appels.

Avant
Une fonction de rappel anonyme
Après
Une fonction de rappel nommée

Allez plus loin

Récapitulons. Voici les types de traitements asynchrones pour lesquels les DevTools vous afficheront une pile d’appels intégrale :

  • Timers : Revenez à l’origine du setTimeout() ou du setInterval().
  • XHRs : Revenez à l’appel xhr.send() qui a déclenché Ajax.
  • Étapes d’animation : Revenez à la ligne d’appel de requestAnimationFrame.
  • Promesses : Revenez à la résolution (ou au rejet) de la promesse.
  • Object.observe() : Revenez à l’endroit qui avait défini le rappel d’observation.
  • MutationObserver : Revenez à l’endroit qui a déclenché l’événement de mutation.
  • window.postMessage() : Traversez les envois de message intra-process.
  • DataTransferItem.getAsString()
  • API FileSystem
  • IndexedDB
  • WebSQL
  • Événements DOM écoutés par addEventListener() : Revenez au site de déclenchement de l’événement. Pour des raisons de performance, tous les événements DOM ne sont pas éligibles à un suivi asynchrone. Parmi ceux qui le sont, on trouve par exemple 'scroll', 'hashchange', et 'selectionchange'.
  • Événements multimédia écoutés par addEventListener() : Revenez au site de déclenchement de l’événement. Les événements multimédia disponibles incluent : les événements audio/vidéo (ex. 'play', 'pause', 'ratechange'), les événements de MediaStreamTrackList en WebRTC (ex. 'addtrack', 'removetrack') et les événements MediaSource (ex. 'sourceopen').

Pouvoir enfin visualiser les piles d’appels intégrales de nos rappels JavaScript devrait nous épargner une calvitie précoce. Cette fonctionnalité des DevTools est particulièrement utile pour déboguer plusieurs événements asynchrones en relation les uns aux autres, ou lorsqu’une exception non capturée est levée depuis du code asynchrone.

Essayez ça dans Chrome. Si vous avez des retours à nous faire sur cette nouvelle possibilité, envoyez-nous un mot sur le bug tracker des Chrome DevTools ou dans le groupe de discussion Chrome DevTools.

Comments

0