Pour démontrer lâutilisation des callbacks, des promesses et dâautres concepts abstraits, nous utiliserons certaines méthodes du navigateur : plus précisément, nous chargerons des scripts et effectuerons des manipulations simples de documents.
Si vous nâêtes pas familier avec ces méthodes, et que leur utilisation dans les exemples est confuse, vous pouvez lire quelques chapitres de la partie suivante du tutoriel.
Mais nous allons quand même essayer de rendre les choses claires. Il nây aura rien de vraiment complexe au niveau du navigateur.
De nombreuses fonctions sont fournies par les environnements hôtes JavaScript qui vous permettent de planifier des actions asynchrones. En dâautres termes, des actions que nous lançons maintenant, mais qui se terminent plus tard.
Par exemple, 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).
Regardez la fonction loadScript(src), qui charge un script avec le src donné:
function loadScript(src) {
// crée une balise <script> et l'ajoute à la page
// ceci fait que le script avec la src donnée commence à se charger et s'exécute une fois terminé.
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
Il insère dans le document une nouvelle balise, créée dynamiquement, <script src="..."> avec le src donné. Le navigateur commence automatiquement à la charger et lâexécute lorsquâelle est terminée.
Nous pouvons utiliser cette fonction comme suit :
// charger et exécuter 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 sous loadScript(...), il nâattend 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 dès quâil est chargé. Il déclare de nouvelles fonctions, et nous voulons les exécuter.
Mais si nous le faisons immédiatement après lâappel loadScript(...), cela ne fonctionnera pas:
loadScript('/my/script.js'); // le script a "function newFunction() {â¦}"
newFunction(); // 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 second argument à loadScript qui doit sâexécuter 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);
}
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 depuis le script, nous devons lâécrire dans le callback:
loadScript('/my/script.js', function() {
// le callback est exécuté après le chargement du script
newFunction(); // maintenant cela fonctionne
...
});
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 :
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 déclarée dans le script chargé
});
Câest ce quâon appelle un style de programmation asynchrone basé sur les âcallbacksâ. Une fonction qui fait quelque chose de manière asynchrone doit fournir un argument callback où nous mettons la fonction à exécuter après quâelle soit terminée.
Ici nous lâavons fait dans loadScript, mais bien sûr câest une approche générale.
Callback imbriqué
Comment charger deux scripts de manière séquentielle: le premier, puis le second après lui ?
La solution naturelle serait de placer le second appel loadScript à lâintérieur 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 après que tous les scripts soient chargés
});
});
});
Ainsi, chaque nouvelle action se trouve dans une callback. Câest bien pour peu dâactions, mais pas pour beaucoup, donc nous verrons bientôt dâautres variantes.
Gestion des erreurs
Dans les exemples ci-dessus, nous nâavons pas tenu compte des erreurs. Que se passe-t-il si le chargement du script échoue ? Notre callback doit être capable de réagir à cette situation.
Voici une version améliorée 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 réussi et callback(error) dans le cas contraire.
Lâutilisation:
loadScript('/my/script.js', function(error, script) {
if (error) {
// erreur dans le chargement du script
} else {
// script chargé avec succès
}
});
Une fois encore, la recette que nous avons utilisée pour loadScript est en fait assez commune. Câest le style âerror-first callbackâ.
La convention est:
- Le premier argument de la
callbackest réservé pour une erreur si elle se produit. Ensuite,callback(err)est appelé. - Le deuxième argument (et les suivants si nécessaire) sont pour le résultat réussi. Ensuite,
callback(null, result1, result2...)est appelé.
Ainsi, la fonction unique callback est utilisée à la fois pour signaler les erreurs et pour renvoyer les résultats.
Pyramide du malheur
à première vue, il sâagit dâun moyen viable de codage asynchrone. Et câest effectivement le cas. Pour un ou peut-être deux appels imbriqués, cela semble correct.
Mais pour de multiples actions asynchrones qui se succèdent, 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 après que tous les scripts soient chargés (*)
}
});
}
});
}
});
Dans le code ci-dessus:
- Nous chargeons
1.js, puis sâil nây a pas dâerreur ⦠- Nous chargeons
2.js, puis sâil nây a pas dâerreur ⦠- Nous chargeons
3.js, puis sâil nây a pas dâerreur â fait autre chose(*).
Au fur et à 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, des déclarations conditionnelles et ainsi de suite.
Câest ce quâon appelle parfois âlâenfer du rappelâ ou âla pyramide du malheurâ.
La âpyramideâ dâappels imbriqués croît vers la droite à chaque action asynchrone. Bientôt, elle devient incontrôlable.
Donc cette façon de coder nâest pas très bonne.
Nous pouvons essayer dâatténuer le problème 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 après que tous les scripts soient chargés (*)
}
}
Vous voyez ? Il fait la même chose, et il nây a pas dâimbrication profonde maintenant parce que nous avons fait de chaque action une fonction séparée de haut niveau.
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ù sauter du regard.
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âencombrement de lâespace de noms ici.
Nous aimerions avoir quelque chose de mieux.
Heureusement, il existe dâautres moyens dâéviter de telles pyramides. Lâun des meilleurs moyens est dâutiliser des âpromessesâ, décrites 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â¦)