web-dev-qa-db-fra.com

setTimeout ne fonctionne pas à l'intérieur de forEach

J'ai un forEach qui appelle une fonction. Il doit y avoir un délai entre chaque appel. Je l'ai mis dans un setTimeout à l'intérieur du forEach. Il ne respecte pas le délai d'attente après la première attente. Au lieu de cela, il attend une fois, puis s'exécute en même temps. J'ai réglé le délai d'attente à 5 secondes et j'utilise une console pour confirmer. 5 secondes d'attente, puis plusieurs journaux de la console foobar à la fois.

Pourquoi est-ce que j'obtiens ce comportement?

var index = 0;
json.objects.forEach(function(obj) {
    setTimeout(function(){
        console.log('foobar');
        self.insertDesignJsonObject(obj, index);
    }, 5000);
});
24
Goose

Ce que Jason a dit est totalement correct dans sa réponse, mais j'ai pensé que je pourrais lui donner un coup de feu, pour mieux clarifier.

Il s'agit en fait d'un problème de fermeture classique. En général, cela ressemblerait à quelque chose comme:

for(var i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i);
    },i * 1000)
}

Le novice s'attendrait à ce que la console affiche:

0
(0 seconds pass...)
1
(1 second passes...)
2
(etc..)

Mais ce n'est tout simplement pas le cas! Ce que vous verriez réellement, c'est le nombre 10 se connecter 10 fois (1x par seconde)!

"Pourquoi cela arrive-t-il?" Grande question. Portée de fermeture. La boucle for ci-dessus n'a pas de portée de fermeture car en javascript, seules les fonctions (lambdas) ont une portée de fermeture!

Voir: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

Pourtant! Votre tentative aurait atteint le résultat souhaité si vous aviez essayé ceci:

    json.objects.forEach(function(obj,index,collection) {
        setTimeout(function(){
            console.log('foobar');
            self.insertDesignJsonObject(obj, index);
        }, index * 5000);
    });

Parce que vous avez accès à la variable "closur-ed" index - vous pouvez compter sur son état comme étant l'état attendu lorsque la fonction (lambda) est invoquée!

Autres ressources:

Comment fonctionnent les fermetures JavaScript?

http://javascript.info/tutorial/closures

http://code.tutsplus.com/tutorials/closures-front-to-back--net-24869

28
The Dembinski

setTimeout est aync. Ce qu'il fait, c'est enregistrer une fonction de rappel et la mettre en arrière-plan qui sera déclenchée après un délai. Alors que forEach est une fonction synchrone. Donc, ce que votre code a fait, c'est d'enregistrer les rappels "d'un seul coup" que chacun sera déclenché après 5 secondes.

Deux façons de l'éviter:

Avoir un index pour régler la minuterie.

json.objects.forEach(function(obj, index) {
    setTimeout(function(){
      // do whatever
    }, 5000 * (index + 1));
});

De cette façon, le facteur de retard est basé sur l'index de vos objets, donc même si vous les enregistrez en même temps, il se déclenchera en fonction de leur propre retard. index + 1 pour conserver le même résultat que celui en question puisqu'il commence à 0.

setInterval pour boucler vos objets

var i = 0;
var interval = setInterval(function(){
    var obj = json.objects[i];
    // do whatever
    i++;
    if(i === json.objects.length) clearInterval(interval);
}, 5000);

setInterval est similaire à setTimeout, bien qu'il se déclenche périodiquement en fonction de l'intervalle. Ici, nous accédons à l'objet et mettons à jour l'index à l'intérieur de la fonction d'intervalle. N'oubliez pas non plus d'effacer l'intervalle à la fin.

La différence entre ces deux est que setInterval n'a enregistré qu'une seule fonction par rapport à setTimeout enregistré autant que le nombre d'éléments dans la liste.

15
Mengo