web-dev-qa-db-fra.com

Appel d'une fonction asynchrone dans une boucle for en JavaScript

J'ai le code suivant:

for(var i = 0; i < list.length; i++){
    mc_cli.get(list[i], function(err, response) {
        do_something(i);
    });
}

mc_cli est une connexion à une base de données memcached. Comme vous pouvez l’imaginer, la fonction de rappel est asynchrone. Elle peut donc être exécutée lorsque la boucle for est déjà terminée. De plus, lors de l'appel de cette manière, do_something(i) utilise toujours la dernière valeur de la boucle for.

J'ai essayé avec une fermeture de cette façon 

do_something((function(x){return x})(i)) 

mais apparemment, ceci utilise toujours la dernière valeur de l'index de la boucle for. 

J'ai aussi essayé de déclarer une fonction avant la boucle for comme ceci:

var create_closure = function(i) {
    return function() {
        return i;
    }
}

puis en appelant 

do_something(create_closure(i)())

mais encore une fois sans succès, la valeur de retour étant toujours la dernière valeur de la boucle for.

Quelqu'un peut-il me dire ce que je fais de mal avec les fermetures? Je pensais les comprendre, mais je ne peux pas comprendre pourquoi cela ne fonctionne pas.

47
Masiar

Puisque vous parcourez un tableau, vous pouvez simplement utiliser forEach qui fournit l'élément de liste et l'index dans le rappel. L'itération aura sa propre portée.

list.forEach(function(listItem, index){
  mc_cli.get(listItem, function(err, response) {
    do_something(index);
  });
});
75
Joseph

C'est le paradigme asynchrone de fonction dans une boucle, et je le traite habituellement en utilisant une fonction anonyme-invoquée-immédiatement. Cela garantit que les fonctions asynchrones sont appelées avec la valeur correcte de la variable d'index.

D'accord! Super. Toutes les fonctions asynchrones ont donc été démarrées et la boucle se ferme. Désormais, rien ne permet de savoir quand ces fonctions seront terminées, en raison de leur nature asynchrone, ou dans quel ordre elles seront exécutées. Si vous avez du code qui doit attendre que toutes ces fonctions soient terminées avant de s'exécuter, je vous recommande de garder un simple compte du nombre de fonctions terminées:

var total = parsed_result.list.length;
var count = 0;

for(var i = 0; i < total; i++){
    (function(foo){
        mc_cli.get(parsed_result.list[foo], function(err, response) {
            do_something(foo);
            count++;
            if (count > total - 1) done();
        });
    }(i));
}

// You can guarantee that this function will not be called until ALL of the
// asynchronous functions have completed.
function done() {
    console.log('All data has been loaded :).');
}
42
user3707766

Vous étiez assez proche, mais vous devriez passer la fermeture à get au lieu de la mettre dans le rappel:

function createCallback(i) {
    return function(){
        do_something(i);
    }
}


for(var i = 0; i < list.length; i++){
    mc_cli.get(list[i], createCallback(i));
}
14
Dennis

Je sais que c'est un vieux fil, mais j'ajoute quand même ma réponse. ES2015 let a pour fonction de relier la variable de boucle à chaque itération, de sorte qu'elle conserve la valeur de la variable de boucle dans les rappels asynchrones. Vous pouvez donc essayer la suivante: 

for(let i = 0; i < list.length; i++){
    mc_cli.get(list[i], function(err, response) {
        do_something(i);
    });
}

Quoi qu'il en soit, il est préférable d'utiliser forEach ou de créer une fermeture à l'aide de la fonction immédiatement invoquée, puisque let est une fonctionnalité ES2015 et peut ne pas prendre en charge tous les navigateurs et toutes les implémentations. De ici sous Bindings ->let->for/for-in loop iteration scope, je peux voir qu’il n’est pas pris en charge jusqu’à Edge 13 et même jusqu’à Firefox 49 (je n’ai pas encore vérifié ces navigateurs). Il dit même qu'il n'est pas pris en charge avec le nœud 4, mais j'ai personnellement testé et il semble qu'il soit pris en charge. 

12
vikneshwar

Essayez ceci en utilisant la syntaxe async/await et Promise

(async function() {
    for(var i = 0; i < list.length; i++){
        await new Promise(next => {
            mc_cli.get(list[i], function(err, response) {
                do_something(i); next()
            })
        })
    }
})()

Cela arrêtera la boucle à chaque cycle jusqu'à ce que la fonction next() soit déclenchée

3
Fernando Carvajal

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);       
   });
}

for (let i = 1; i < 5; i++) {
    await XHRpost(i);
   }
2
Sumer

Si vous souhaitez exécuter des fonctions asynchrones dans une boucle tout en conservant l'index ou d'autres variables après l'exécution d'un rappel, vous pouvez envelopper votre code dans une expression IIFE (expression de fonction immédiatement appelée).

var arr = ['Hello', 'World', 'Javascript', 'Async', ':)'];
for( var i = 0; i < arr.length; i++) {
  (function(index){
    setTimeout(function(){
       console.log(arr[index]);
 }, 500);
0
Amay Kulkarni