J'exécute une boucle d'événement 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 indiquant les chiffres de 0 à 10. Le problème est que, au moment où la fonction de rappel est déclenchée, la boucle a déjà effectué plusieurs itérations et affiche une valeur supérieure de i
. Des recommandations sur la façon de résoudre ce problème?
La boucle for
s’exécute immédiatement et toutes vos opérations asynchrones sont démarrées. Lorsqu'ils auront terminé dans le futur et appelé leurs rappels, la valeur de votre variable d'index de boucle i
sera à sa dernière valeur pour tous les rappels.
En effet, la boucle for
n'attend pas la fin d'une opération asynchrone avant de passer à la prochaine itération de la boucle et parce que les rappels asynchrones sont appelés ultérieurement. Ainsi, la boucle termine ses itérations et ALORS les rappels sont appelés à la fin de ces opérations asynchrones. En tant que tel, l'index de boucle est "terminé" et reste à sa valeur finale pour tous les rappels.
Pour contourner ce problème, vous devez enregistrer de manière unique l’index de boucle séparément pour chaque rappel. Pour ce faire, vous devez capturer cette fonction dans une fermeture de fonction. Pour ce faire, vous pouvez créer une fermeture de fonction en ligne spécialement à cet effet (premier exemple présenté ci-dessous) ou créer une fonction externe à laquelle vous transmettez l'index et le laissez gérer l'index de manière unique pour vous (deuxième exemple présenté ci-dessous).
À partir de 2016, si vous avez une implémentation Javascript conforme à la norme ES6, vous pouvez également utiliser let
pour définir la variable de boucle for
. Cette variable sera définie de manière unique pour chaque itération de la boucle for
(troisième implémentation ci-dessous). Notez toutefois qu'il s'agit d'une fonctionnalité d'implémentation tardive dans les implémentations ES6. Vous devez donc vous assurer que votre environnement d'exécution prend en charge cette option.
Utilisez .forEach () pour itérer puisqu'il crée sa propre fonction de fermeture
someArray.forEach(function(item, i) {
asynchronousProcess(function(item) {
console.log(i);
});
});
Créez votre propre fonction de fermeture à l'aide d'un 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 transmettre la variable
Si vous pouvez modifier la fonction asynchronousProcess()
, vous pouvez simplement passer la valeur à cet endroit et demander à la fonction asynchronousProcess()
de retourner la commande cntr au rappel comme ceci:
var j = 10;
for (var i = 0; i < j; i++) {
asynchronousProcess(i, function(cntr) {
console.log(cntr);
});
}
Utilisez ES6 let
Si votre environnement d’exécution Javascript prend entièrement en charge 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);
});
}
let
déclaré dans une déclaration de boucle for
comme celle-ci créera une valeur unique de i
pour chaque appel de la boucle (qui correspond à vos souhaits).
Sérialiser avec des promesses et async/wait
Si votre fonction async retourne une promesse et que vous souhaitez sérialiser vos opérations asynchrones pour qu'elles s'exécutent les unes après les autres plutôt qu'en parallèle et que vous vous exécutiez dans un environnement moderne prenant en charge async
et await
, vous disposez de 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'un seul appel à asynchronousProcess()
est en vol à la fois et que la boucle for
n'avancera même pas avant que chacun d'entre eux soit terminé. Cela diffère des schémas précédents qui exécutaient tous vos opérations asynchrones en parallèle et dépend donc entièrement de la conception souhaitée. Remarque: await
fonctionne avec une promesse. Votre fonction doit donc renvoyer une promesse résolue/rejetée une fois l'opération asynchrone terminée. Notez également que pour utiliser await
, la fonction contenant doit être déclarée async
.
Toute recommandation sur la façon de résoudre ce problème?
Nombreuses. 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, mais Firefox le prend déjà en charge depuis un certain temps), vous pourriez avoir:
for (i = 0; i < j; i++) {
let k = i;
asycronouseProcess(function() {
alert(k);
});
}
Ou, vous pouvez faire le travail de bind
manuellement (si le navigateur ne le supporte pas, mais je dirais que vous pouvez implémenter un shim dans ce cas, il devrait se trouver 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 Firefox add-on); sinon bind
ou une fonction currying function (qui n'a pas besoin d'objet de contexte).
async await
est ici (ES7), vous pouvez donc faire ce genre de choses très facilement maintenant.
var i;
var j = 10;
for (i = 0; i < j; i++) {
await asycronouseProcess();
alert(i);
}
N'oubliez pas que cela ne fonctionne que si asycronouseProcess
renvoie une Promise
Si asycronouseProcess
n'est pas sous votre contrôle, vous pouvez le renvoyer par vous-même Promise
comme ceci
function asyncProcess() {
return new Promise((resolve, reject) => {
asycronouseProcess(()=>{
resolve();
})
})
}
Puis remplacez cette ligne await asycronouseProcess();
par await asyncProcess();
Comprendre Promises
avant même de regarder dans async await
est indispensable (Lire également sur le support de async await
)
var i = 0;
var length = 10;
function for1() {
console.log(i);
for2();
}
function for2() {
if (i == length) {
return false;
}
setTimeout(function() {
i++;
for1();
}, 500);
}
for1();
Voici un exemple d’approche fonctionnelle par rapport à ce qui est attendu ici.
ES2017: Vous pouvez insérer le code async dans une fonction (par exemple, XHRPost) en renvoyant une promesse (code async dans la promesse).
Appelez ensuite la fonction (XHRPost) dans la boucle for, mais avec le mot clé magique Await. :)
let http = new XMLHttpRequest();
let url = 'http://sumersin/forum.social.json';
function XHRpost(i) {
return new Promise(function(resolve) {
let params = 'id=nobot&%3Aoperation=social%3AcreateForumPost&subject=Demo' + i + '&message=Here%20is%20the%20Demo&_charset_=UTF-8';
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
http.onreadystatechange = function() {
console.log("Done " + i + "<<<<>>>>>" + http.readyState);
if(http.readyState == 4){
console.log('SUCCESS :',i);
resolve();
}
}
http.send(params);
});
}
(async () => {
for (let i = 1; i < 5; i++) {
await XHRpost(i);
}
})();
Le code JavaScript s'exécute sur un seul thread. Par conséquent, vous ne pouvez pas principalement bloquer pour attendre la fin de la première itération de la boucle avant de commencer la suivante sans impacter gravement la facilité d'utilisation de la page.
La solution dépend de ce dont vous avez vraiment besoin. Si l'exemple correspond exactement à ce dont vous avez besoin, la suggestion de @ Simon de passer i
à votre processus asynchrone est judicieuse.