Pour dmontrer lutilisation des callbacks, des promesses et dautres concepts abstraits, nous utiliserons certaines mthodes du navigateur : plus prcisment, nous chargerons des scripts et effectuerons des manipulations simples de documents.
Si vous ntes pas familier avec ces mthodes, et que leur utilisation dans les exemples est confuse, vous pouvez lire quelques chapitres de la partie suivante du tutoriel.
Mais nous allons quand mme essayer de rendre les choses claires. Il ny aura rien de vraiment complexe au niveau du navigateur.
De nombreuses fonctions sont fournies par les environnements htes JavaScript qui vous permettent de planifier des actions asynchrones. En dautres termes, des actions que nous lanons maintenant, mais qui se terminent plus tard.
Par exemple, une de ces fonctions est la fonction setTimeout.
Il existe dautres exemples concrets dactions asynchrones, par exemple le chargement de scripts et de modules (nous les aborderons dans les chapitres suivants).
Regardez la fonction loadScript(src), qui charge un script avec le src donn:
function loadScript(src) {
// cre une balise <script> et l'ajoute la page
// ceci fait que le script avec la src donne commence se charger et s'excute une fois termin.
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
Il insre dans le document une nouvelle balise, cre dynamiquement, <script src="..."> avec le src donn. Le navigateur commence automatiquement la charger et lexcute lorsquelle est termine.
Nous pouvons utiliser cette fonction comme suit :
// charger et excuter le script au chemin donn
loadScript('/my/script.js');
Le script est excut de manire asynchrone, car il commence se charger maintenant, mais sexcute plus tard, lorsque la fonction est dj termine.
Sil y a du code sous loadScript(...), il nattend pas que le chargement du script soit termin.
loadScript('/my/script.js');
// le code dessous loadScript
// n'attend pas que le chargement du script soit termin
// ...
Disons que nous devons utiliser le nouveau script ds quil est charg. Il dclare de nouvelles fonctions, et nous voulons les excuter.
Mais si nous le faisons immdiatement aprs lappel loadScript(...), cela ne fonctionnera pas:
loadScript('/my/script.js'); // le script a "function newFunction() {}"
newFunction(); // aucune fonction de ce type!
Naturellement, le navigateur na probablement pas eu le temps de charger le script. Pour linstant, la fonction loadScript ne permet pas de suivre lachvement du chargement. Le script se charge et finit par sexcuter, cest tout. Mais nous aimerions savoir quand cela se produit, pour utiliser les nouvelles fonctions et variables de ce script.
Ajoutons une fonction callback comme second argument loadScript qui doit sexcuter lorsque le script se charge :
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
Lvnement onload est dcrit dans larticle Chargement des ressources: onload et onerror, il excute essentiellement une fonction aprs le chargement et lexcution du script.
Maintenant, si nous voulons appeler de nouvelles fonctions depuis le script, nous devons lcrire dans le callback:
loadScript('/my/script.js', function() {
// le callback est excut aprs le chargement du script
newFunction(); // maintenant cela fonctionne
...
});
Cest lide: le deuxime argument est une fonction (gnralement anonyme) qui sexcute lorsque laction est termine.
Voici un exemple excutable avec un vrai script :
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`Cool, the script ${script.src} is loaded`);
alert( _ ); // _ est une fonction dclare dans le script charg
});
Cest ce quon appelle un style de programmation asynchrone bas sur les callbacks. Une fonction qui fait quelque chose de manire asynchrone doit fournir un argument callback o nous mettons la fonction excuter aprs quelle soit termine.
Ici nous lavons fait dans loadScript, mais bien sr cest une approche gnrale.
Callback imbriqu
Comment charger deux scripts de manire squentielle: le premier, puis le second aprs lui ?
La solution naturelle serait de placer le second appel loadScript lintrieur du callback, comme ceci:
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's load one more`);
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
});
Une fois que le loadScript externe est termin, le callback lance le loadScript interne.
Et si nous voulons un script de plus ?
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...continue aprs que tous les scripts soient chargs
});
});
});
Ainsi, chaque nouvelle action se trouve dans une callback. Cest bien pour peu dactions, mais pas pour beaucoup, donc nous verrons bientt dautres variantes.
Gestion des erreurs
Dans les exemples ci-dessus, nous navons pas tenu compte des erreurs. Que se passe-t-il si le chargement du script choue ? Notre callback doit tre capable de ragir cette situation.
Voici une version amliore de loadScript qui suit les erreurs de chargement :
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
Il appelle callback(null, script) en cas de chargement russi et callback(error) dans le cas contraire.
Lutilisation:
loadScript('/my/script.js', function(error, script) {
if (error) {
// erreur dans le chargement du script
} else {
// script charg avec succs
}
});
Une fois encore, la recette que nous avons utilise pour loadScript est en fait assez commune. Cest le style error-first callback.
La convention est:
- Le premier argument de la
callbackest rserv pour une erreur si elle se produit. Ensuite,callback(err)est appel. - Le deuxime argument (et les suivants si ncessaire) sont pour le rsultat russi. Ensuite,
callback(null, result1, result2...)est appel.
Ainsi, la fonction unique callback est utilise la fois pour signaler les erreurs et pour renvoyer les rsultats.
Pyramide du malheur
premire vue, il sagit dun moyen viable de codage asynchrone. Et cest effectivement le cas. Pour un ou peut-tre deux appels imbriqus, cela semble correct.
Mais pour de multiples actions asynchrones qui se succdent, nous aurons un code comme celui-ci:
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue aprs que tous les scripts soient chargs (*)
}
});
}
});
}
});
Dans le code ci-dessus:
- Nous chargeons
1.js, puis sil ny a pas derreur - Nous chargeons
2.js, puis sil ny a pas derreur - Nous chargeons
3.js, puis sil ny a pas derreur fait autre chose(*).
Au fur et mesure que les appels deviennent plus imbriqus, le code devient plus profond et de plus en plus difficile grer, surtout si nous avons du vrai code au lieu de ... qui peut inclure plus de boucles, des dclarations conditionnelles et ainsi de suite.
Cest ce quon appelle parfois lenfer du rappel ou la pyramide du malheur.
La pyramide dappels imbriqus crot vers la droite chaque action asynchrone. Bientt, elle devient incontrlable.
Donc cette faon de coder nest pas trs bonne.
Nous pouvons essayer dattnuer le problme en faisant de chaque action une fonction autonome, comme ceci:
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...continue aprs que tous les scripts soient chargs (*)
}
}
Vous voyez ? Il fait la mme chose, et il ny a pas dimbrication profonde maintenant parce que nous avons fait de chaque action une fonction spare de haut niveau.
Cela fonctionne, mais le code ressemble une feuille de calcul dchire. Il est difficile lire, et vous avez probablement remarqu quil faut passer dun morceau lautre en le lisant. Ce nest pas pratique, surtout si le lecteur nest pas familier avec le code et ne sait pas o sauter du regard.
De plus, les fonctions nommes step* sont toutes usage unique, elles sont cres uniquement pour viter la pyramide du malheur. Personne ne va les rutiliser en dehors de la chane daction. Il y a donc un peu dencombrement de lespace de noms ici.
Nous aimerions avoir quelque chose de mieux.
Heureusement, il existe dautres moyens dviter de telles pyramides. Lun des meilleurs moyens est dutiliser des promesses, dcrites dans le chapitre suivant.
Commentaires
<code>, pour plusieurs lignes enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen)