Nous utilisons des méthodes de navigateur dans les exemples ici
Pour démontrer l'utilisation des rappels, des promesses et d'autres concepts abstraits, nous utiliserons certaines méthodes de navigateur : en particulier le chargement de scripts et l'exécution de manipulations simples de documents.
Si vous n'êtes pas familier avec ces méthodes et que leur utilisation dans les exemples prête à confusion, vous souhaiterez peut-être lire quelques chapitres de la partie suivante du didacticiel.
Mais nous allons quand même essayer de clarifier les choses. Il n'y aura rien de vraiment complexe au niveau du navigateur.
De nombreuses fonctions sont fournies par les environnements hôtes JavaScript qui permettent de planifier des actions asynchrones . En d’autres termes, des actions que nous commençons maintenant, mais qui se terminent plus tard.
Par exemple, l’une de ces fonctions est la fonction setTimeout
.
Il existe d'autres exemples concrets d'actions asynchrones, par exemple le chargement de scripts et de modules (nous les aborderons dans les chapitres suivants).
Jetez un œil à la fonction loadScript(src)
, qui charge un script avec le src
donné :
fonction loadScript(src) { // crée une balise <script> et l'ajoute à la page // cela provoque le démarrage du chargement du script avec le src donné et son exécution une fois terminé let script = document.createElement('script'); script.src = src; document.head.append(script); }
Il insère dans le document une nouvelle balise <script src="…">
créée dynamiquement avec le src
donné. Le navigateur commence automatiquement à le charger et s'exécute une fois terminé.
Nous pouvons utiliser cette fonction comme ceci :
// charge et exécute le script au chemin donné loadScript('/my/script.js');
Le script est exécuté « de manière asynchrone », car il commence à se charger maintenant, mais s'exécute plus tard, lorsque la fonction est déjà terminée.
S'il y a du code en dessous loadScript(…)
, il n'attend pas la fin du chargement du script.
loadScript('/my/script.js'); // le code ci-dessous loadScript // n'attend pas la fin du chargement du script //...
Disons que nous devons utiliser le nouveau script dès son chargement. Il déclare de nouvelles fonctions et nous voulons les exécuter.
Mais si nous faisons cela immédiatement après l'appel loadScript(…)
, cela ne fonctionnera pas :
loadScript('/my/script.js'); // le script a "function newFunction() {…}" nouvelleFonction(); // aucune fonction de ce type !
Naturellement, le navigateur n'a probablement pas eu le temps de charger le script. Pour l'instant, la fonction loadScript
ne permet pas de suivre l'achèvement du chargement. Le script se charge et finit par s'exécuter, c'est tout. Mais nous aimerions savoir quand cela se produit, pour utiliser les nouvelles fonctions et variables de ce script.
Ajoutons une fonction callback
comme deuxième argument à loadScript
qui doit s'exécuter lors du chargement du script :
fonction loadScript (src, rappel) { let script = document.createElement('script'); script.src = src; script.onload = () => rappel(script); document.head.append(script); }
L'événement onload
est décrit dans l'article Chargement des ressources : onload et onerror, il exécute essentiellement une fonction après le chargement et l'exécution du script.
Maintenant, si nous voulons appeler de nouvelles fonctions à partir du script, nous devons l'écrire dans le rappel :
loadScript('/my/script.js', function() { // le rappel s'exécute après le chargement du script nouvelleFonction(); // donc maintenant ça marche ... });
C'est l'idée : le deuxième argument est une fonction (généralement anonyme) qui s'exécute lorsque l'action est terminée.
Voici un exemple exécutable avec un vrai script :
fonction loadScript (src, rappel) { let script = document.createElement('script'); script.src = src; script.onload = () => rappel(script); document.head.append(script); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`Cool, le script ${script.src} est chargé`); alerte( _ ); // _ est une fonction déclarée dans le script chargé });
C'est ce qu'on appelle un style de programmation asynchrone « basé sur le rappel ». Une fonction qui fait quelque chose de manière asynchrone doit fournir un argument callback
dans lequel nous mettons la fonction à s'exécuter une fois qu'elle est terminée.
Ici, nous l'avons fait dans loadScript
, mais bien sûr, c'est une approche générale.
Comment pouvons-nous charger deux scripts séquentiellement : le premier, puis le second après ?
La solution naturelle serait de placer le deuxième appel loadScript
dans le rappel, comme ceci :
loadScript('/my/script.js', fonction(script) { alert(`Cool, le ${script.src} est chargé, chargeons-en un de plus`); loadScript('/my/script2.js', fonction(script) { alert(`Cool, le deuxième script est chargé`); }); });
Une fois le loadScript
externe terminé, le rappel lance le LoadScript interne.
Et si nous voulons un script de plus… ?
loadScript('/my/script.js', fonction(script) { loadScript('/my/script2.js', fonction(script) { loadScript('/my/script3.js', fonction(script) { // ... continue une fois tous les scripts chargés }); }); });
Ainsi, chaque nouvelle action se trouve dans un rappel. C'est bien pour quelques actions, mais pas pour beaucoup, nous verrons donc bientôt d'autres variantes.
Dans les exemples ci-dessus, nous n'avons pas pris en compte les erreurs. Que se passe-t-il si le chargement du script échoue ? Notre rappel devrait pouvoir réagir à cela.
Voici une version améliorée de loadScript
qui suit les erreurs de chargement :
fonction loadScript (src, rappel) { let script = document.createElement('script'); script.src = src; script.onload = () => rappel (null, script); script.onerror = () => callback(new Error(`Erreur de chargement de script pour ${src}`)); document.head.append(script); }
Il appelle callback(null, script)
pour un chargement réussi et callback(error)
dans le cas contraire.
L'utilisation:
loadScript('/my/script.js', function(erreur, script) { si (erreur) { // gérer l'erreur } autre { // script chargé avec succès } });
Encore une fois, la recette que nous avons utilisée pour loadScript
est en fait assez courante. C'est ce qu'on appelle le style « rappel d'abord en cas d'erreur ».
La convention est :
Le premier argument du callback
est réservé à une erreur si elle se produit. Ensuite, callback(err)
est appelé.
Le deuxième argument (et les suivants si nécessaire) concerne le résultat positif. Ensuite, callback(null, result1, result2…)
est appelé.
Ainsi, la fonction callback
unique est utilisée à la fois pour signaler les erreurs et renvoyer les résultats.
À première vue, cela semble être une approche viable du codage asynchrone. Et c’est effectivement le cas. Pour un ou peut-être deux appels imbriqués, cela semble correct.
Mais pour plusieurs actions asynchrones qui se succèdent, nous aurons un code comme celui-ci :
loadScript('1.js', fonction(erreur, script) { si (erreur) { handleError(erreur); } autre { //... loadScript('2.js', fonction(erreur, script) { si (erreur) { handleError(erreur); } autre { //... loadScript('3.js', fonction(erreur, script) { si (erreur) { handleError(erreur); } autre { // ... continue une fois tous les scripts chargés (*) } }); } }); } });
Dans le code ci-dessus :
On charge 1.js
, puis s'il n'y a pas d'erreur…
On charge 2.js
, puis s'il n'y a pas d'erreur…
Nous chargeons 3.js
, puis s'il n'y a pas d'erreur, faites autre chose (*)
.
À mesure que les appels deviennent plus imbriqués, le code devient plus profond et de plus en plus difficile à gérer, surtout si nous avons du vrai code au lieu de ...
qui peut inclure plus de boucles, d'instructions conditionnelles, etc.
C'est ce qu'on appelle parfois « l'enfer des rappels » ou la « pyramide du malheur ».
La « pyramide » des appels imbriqués s'agrandit vers la droite à chaque action asynchrone. Bientôt, la situation devient incontrôlable.
Cette façon de coder n’est donc pas très bonne.
Nous pouvons essayer de résoudre le problème en faisant de chaque action une fonction autonome, comme ceci :
loadScript('1.js', étape1); fonction étape 1 (erreur, script) { si (erreur) { handleError(erreur); } autre { //... loadScript('2.js', étape2); } } fonction step2 (erreur, script) { si (erreur) { handleError(erreur); } autre { //... loadScript('3.js', étape3); } } fonction step3 (erreur, script) { si (erreur) { handleError(erreur); } autre { // ... continue une fois tous les scripts chargés (*) } }
Voir? Cela fait la même chose, et il n’y a plus d’imbrication profonde maintenant car nous avons fait de chaque action une fonction distincte de niveau supérieur.
Cela fonctionne, mais le code ressemble à une feuille de calcul déchirée. Il est difficile à lire et vous avez probablement remarqué qu'il faut passer d'un morceau à l'autre en le lisant. Ce n'est pas pratique, surtout si le lecteur n'est pas familier avec le code et ne sait pas où jeter les yeux.
De plus, les fonctions nommées step*
sont toutes à usage unique, elles sont créées uniquement pour éviter la « pyramide du malheur ». Personne ne va les réutiliser en dehors de la chaîne d’action. Il y a donc un peu d'espace de noms encombré ici.
Nous aimerions avoir quelque chose de mieux.
Heureusement, il existe d’autres moyens d’éviter de telles pyramides. L’un des meilleurs moyens consiste à utiliser les « promesses », décrites dans le chapitre suivant.