Processus asynchrone à l'intérieur d'une boucle for en JavaScript
Je fais fonctionner une boucle d'événements de la forme suivante :
var i;
var j = 10;
for (i = 0; i < j; i++) {
asynchronousProcess(callbackFunction() {
alert(i);
});
}
J'essaie d'afficher une série d'alertes montrant les numéros de 0 à 10. Le problème est qu'au moment où la fonction de rappel est déclenchée, la boucle a déjà passé quelques itérations et elle affiche une valeur plus élevée de i. Des recommandations pour résoudre ce problème ?
Réponse #1
La boucle for s'exécute immédiatement jusqu'à son terme tandis que toutes vos opérations asynchrones sont démarrées. Lorsqu'elles se terminent à un moment donné dans le futur et appellent leurs callbacks, la valeur de votre variable d'index de boucle i sera à sa dernière valeur pour tous les callbacks.
Ceci est dû au fait que la boucle for n'attend pas qu'une opération asynchrone soit terminée avant de passer à l'itération suivante de la boucle et que les rappels asynchrones sont appelés quelque temps dans le futur. Ainsi, la boucle termine ses itérations et ENSUITE les rappels sont appelés lorsque ces opérations asynchrones se terminent. Par conséquent, l' index de la boucle est « terminé » et se trouve à sa valeur finale pour tous les rappels.
Pour contourner cela, vous devez sauvegarder de manière unique l'index de la boucle séparément pour chaque rappel. En Javascript, la façon de le faire est de le capturer dans une fermeture de fonction. Cela peut être fait soit en créant une fermeture de fonction en ligne spécifiquement à cet effet (premier exemple montré ci-dessous), soit en créant une fonction externe à laquelle vous passez l'index et qui permet de maintenir l'index de manière unique pour vous (deuxième exemple montré ci-dessous).
Depuis 2016, si vous disposez d'une implémentation ES6 pleinement conforme de Javascript, vous pouvez également utiliser let pour définir la variable de la boucle for et elle sera définie de manière unique pour chaque itération de la boucle for (troisième implémentation ci-dessous). Cependant, notez qu'il s'agit d'une fonctionnalité implémentée tardivement dans les implémentations ES6, donc vous devez vous assurer que votre environnement d'exécution prend en charge cette option.
Utilisez .forEach() pour itérer puisque cela crée sa propre fermeture de fonction
someArray.forEach(function(item, i) {
asynchronousProcess(function(item) {
console.log(i);
});
});
Créez votre propre fermeture de fonction en utilisant une IIFE
var j = 10;
for (var i = 0; i < j; i++) {
(function(cntr) {
// here the value of i was passed into as the argument cntr
// and will be captured in this function closure so each
// iteration of the loop can have it's own value
asynchronousProcess(function() {
console.log(cntr);
});
})(i);
}
Créer ou modifier une fonction externe et lui passer la variable
Si vous pouvez modifier la fonction asynchronousProcess(), alors vous pourriez simplement y passer la valeur et faire en sorte que la fonction asynchronousProcess() retourne le cntr au rappel de cette manière :
var j = 10;
for (var i = 0; i < j; i++) {
asynchronousProcess(i, function(cntr) {
console.log(cntr);
});
}
Utilisez ES6 let
Si vous disposez d'un environnement d'exécution Javascript qui prend en charge pleinement ES6, vous pouvez utiliser let dans votre boucle for comme ceci :
const j = 10;
for (let i = 0; i < j; i++) {
asynchronousProcess(function() {
console.log(i);
});
}
La déclaration let dans une déclaration de boucle for comme celle-ci créera une valeur unique de i pour chaque itération de la boucle (ce que vous voulez).
Sérialisation avec les promesses et async/await
Si votre fonction asynchrone retourne une promesse et que vous voulez sérialiser vos opérations asynchrones pour qu'elles s'exécutent les unes après les autres au lieu de parallèlement, et que vous êtes dans un environnement moderne qui supporte async et await, alors vous avez plus d'options.
async function someFunction() {
const j = 10;
for (let i = 0; i < j; i++) {
// wait for the promise to resolve before advancing the for loop
await asynchronousProcess();
console.log(i);
}
}
Cela garantira qu'une seule appel à asynchronousProcess() est en cours à la fois et que la boucle for n'avancera même pas tant que chacun n'est pas terminé. C'est différent des schémas précédents qui exécutaient toutes vos opérations asynchrones en parallèle, donc cela dépend entièrement du design que vous souhaitez. Remarque : await fonctionne avec une promesse, donc votre fonction doit retourner une promesse qui est résolue/rejetée lorsque l'opération asynchrone est terminée. De plus, notez que pour utiliser await, la fonction contenant doit être déclarée async.
Exécutez des opérations asynchrones en parallèle et utilisez Promise.all() pour collecter les résultats dans l'ordre
function someFunction() {
let promises = [];
for (let i = 0; i < 10; i++) {
promises.push(asynchonousProcessThatReturnsPromise());
}
return Promise.all(promises);
}
someFunction().then(results => {
// array of results in order here
console.log(results);
}).catch(err => {
console.log(err);
});
Réponse #2
async await est ici (ES7), donc vous pouvez faire ce genre de choses très facilement maintenant.
var i;
var j = 10;
for (i = 0; i < j; i++) {
await asycronouseProcess();
alert(i);
}
Rappelez-vous, cela ne fonctionne que si asycronouseProcess renvoie une Promise
Si asycronouseProcess n'est pas sous votre contrôle, vous pouvez le faire retourner un Promise vous-même comme ceci
function asyncProcess() {
return new Promise((resolve, reject) => {
asycronouseProcess(()=>{
resolve();
})
})
}
Remplacez ensuite cette ligne await asycronouseProcess(); par await asyncProcess();
Comprendre les Promises avant même de se pencher sur async await est indispensable (Lisez également sur le support pour async await)
Réponse #3
Une recommandation sur la façon de réparer cela ?
Plusieurs. Vous pouvez utiliser bind :
for (i = 0; i < j; i++) {
asycronouseProcess(function (i) {
alert(i);
}.bind(null, i));
}
Ou, si votre navigateur prend en charge let (ce sera dans la prochaine version d'ECMAScript, cependant Firefox le supporte déjà depuis un certain temps) vous pourriez avoir :
for (i = 0; i < j; i++) {
let k = i;
asycronouseProcess(function() {
alert(k);
});
}
Ou, vous pourriez faire le travail de bind manuellement (au cas où le navigateur ne le supporterait pas, mais je dirais que vous pouvez implémenter un shim dans ce cas, il devrait être dans le lien ci-dessus) :
for (i = 0; i < j; i++) {
asycronouseProcess(function(i) {
return function () {
alert(i)
}
}(i));
}
Je préfère généralement let quand je peux l'utiliser (par exemple pour un module complémentaire Firefox); sinon bind ou une fonction de currying personnalisée (qui n'a pas besoin d'un objet contexte).
