87 votes

Cycle asynchrone en JavaScript

J'ai besoin d'une boucle qui attend un appel asynchrone avant de continuer. Quelque chose comme :

for ( /* ... */ ) {

  someFunction(param1, praram2, function(result) {

    // D'accord, la boucle pourrait continuer

  })

}

alert("Boucle for terminée");

Comment pourrais-je faire cela ? Avez-vous des idées ?

128 votes

Wow ( /* ... */ ) ressemble à un monstre et j'ai peur maintenant :(

182voto

Ivo Wetzel Points 27802

Vous ne pouvez pas mélanger le synchrone et l'asynchrone en JavaScript, si vous bloquez le script, vous bloquez le navigateur.

Vous devez adopter complètement la façon basée sur les événements ici, heureusement que nous pouvons cacher les parties laides.

EDIT: Code mis à jour.

function asyncLoop(iterations, func, callback) {
    var index = 0;
    var done = false;
    var loop = {
        next: function() {
            if (done) {
                return;
            }

            if (index < iterations) {
                index++;
                func(loop);

            } else {
                done = true;
                callback();
            }
        },

        iteration: function() {
            return index - 1;
        },

        break: function() {
            done = true;
            callback();
        }
    };
    loop.next();
    return loop;
}

Cela nous fournira une boucle asynchrone loop, vous pouvez bien sûr le modifier encore davantage pour prendre par exemple une fonction pour vérifier la condition de la boucle, etc.

Maintenant, passons au test:

function someFunction(a, b, callback) {
    console.log('Hey doing some stuff!');
    callback();
}

asyncLoop(10, function(loop) {
    someFunction(1, 2, function(result) {

        // log the iteration
        console.log(loop.iteration());

        // Okay, for cycle could continue
        loop.next();
    })},
    function(){console.log('cycle ended')}
);

Et la sortie:

Hey doing some stuff!
0
Hey doing some stuff!
1
Hey doing some stuff!
2
Hey doing some stuff!
3
Hey doing some stuff!
4
Hey doing some stuff!
5
Hey doing some stuff!
6
Hey doing some stuff!
7
Hey doing some stuff!
8
Hey doing some stuff!
9
cycle ended

28 votes

Peut-être que je rate quelque chose, mais je ne comprends pas comment cela est vraiment asynchrone. N'avez-vous pas besoin d'un setTimeout ou quelque chose comme ça? J'ai essayé votre code, enlevé le console.log, et augmenté le compteur beaucoup et cela bloque simplement le navigateur.

0 votes

Je suis confus quant à ce que loop.break() est censé faire? Juste un moyen de sortir de force si vous le voulez?

2 votes

Comme le dit rocketsarefast ci-dessus, cette réponse n'est pas asynchrone et donc complètement fausse !

44voto

wilsonpage Points 5495

J'ai simplifié ceci:

FONCTION:

var asyncLoop = function(o){
    var i=-1;

    var loop = function(){
        i++;
        if(i==o.length){o.callback(); return;}
        o.functionToLoop(loop, i);
    } 
    loop();//init
}

UTILISATION:

asyncLoop({
    length : 5,
    functionToLoop : function(loop, i){
        setTimeout(function(){
            document.write('Itération ' + i + ' ');
            loop();
        },1000);
    },
    callback : function(){
        document.write('Tout est fini!');
    }    
});

EXEMPLE: http://jsfiddle.net/NXTv7/8/

0 votes

+1 J'ai fait quelque chose de similaire à cela, mais j'ai mis la partie setTimeout dans la fonction de la bibliothèque.

0 votes

N'est-ce pas essentiellement une récursion?

7voto

Adam Lassek Points 18918

Une alternative plus propre à ce que @Ivo a suggéré serait une File d'attente de méthodes asynchrones, en supposant que vous n'ayez besoin de faire qu'un seul appel asynchrone pour la collection.

(Voir cet article de Dustin Diaz pour une explication plus détaillée)

function Queue() {
  this._methods = [];
  this._response = null;
  this._flushed = false;
}

(function(Q){

  Q.add = function (fn) {
    if (this._flushed) fn(this._response);
    else this._methods.push(fn);
  }

  Q.flush = function (response) {
    if (this._flushed) return;
    this._response = response;
    while (this._methods[0]) {
      this._methods.shift()(response);
    }
    this._flushed = true;
  }

})(Queue.prototype);

Il vous suffit de créer une nouvelle instance de Queue, d'ajouter les rappels dont vous avez besoin, puis de vider la file d'attente avec la réponse asynchrone.

var queue = new Queue();

queue.add(function(results){
  for (var result in results) {
    // opération de boucle normale ici
  }
});

someFunction(param1, param2, function(results) {
  queue.flush(results);
}

Un avantage supplémentaire de ce modèle est que vous pouvez ajouter plusieurs fonctions à la file d'attente au lieu d'une seule.

Si vous avez un objet contenant des fonctions d'itération, vous pouvez ajouter un support pour cette file d'attente en arrière-plan et écrire du code qui semble synchrone, mais ne l'est pas :

MyClass.each(function(result){ ... })

il suffit d'écrire each pour mettre la fonction anonyme dans la file d'attente au lieu de l'exécuter immédiatement, puis vider la file d'attente lorsque votre appel asynchrone est terminé. Il s'agit d'un modèle de conception très simple et puissant.

P.S. Si vous utilisez jQuery, vous avez déjà à votre disposition une file d'attente de méthodes asynchrones appelée jQuery.Deferred.

1 votes

Eh bien, si j'ai bien compris la question, cela ne donnera pas le comportement souhaité. Il semble qu'elle veuille faire quelques rappels dans someFunction qui retardent le reste de la boucle, votre modèle met en place une liste de fonctions qui seront exécutées dans l'ordre et recevront toutes les résultats d'un autre appel de fonction. C'est un bon modèle mais je ne pense pas qu'il corresponde à la question posée.

0 votes

@Ivo Sans plus d'informations, nous ne saurons pas avec certitude, mais en parlant en général, je pense que c'est une mauvaise conception de faire attendre un code synchrone pour une opération asynchrone avant de continuer; dans tous les cas où je l'ai essayé, cela a entraîné un délai d'UI notable en raison de JS étant mono-thread. Si l'opération prend trop de temps, vous courez le risque que votre script soit arrêté de force par le navigateur.

0 votes

@Ivo je suis également très méfiant à l'égard du code qui dépend de setTimeout. Vous risquez un comportement non désiré si le code s'exécute plus rapidement que prévu.

3voto

Mrchief Points 25418

Regardez aussi cette bibliothèque splendide caolan / async. Votre boucle for peut facilement être accomplie en utilisant mapSeries ou series.

Je pourrais poster un exemple de code si votre exemple contenait plus de détails.

2voto

Nous pouvons également utiliser l'aide de jquery.Deferred. dans ce cas, la fonction asyncLoop ressemblerait à ceci :

asyncLoop = function(array, callback) {
  var nextElement, thisIteration;
  if (array.length > 0) nextElement = array.pop();
  thisIteration = callback(nextElement);
  $.when(thisIteration).done(function(response) {
    // ici nous pouvons vérifier la valeur de la réponse pour breaker ou autre
    if (array.length > 0) asyncLoop(array, collection, callback);
  });
};

La fonction de rappel ressemblerait à ceci :

addEntry = function(newEntry) {
  var deferred, duplicateEntry;
  // à la ligne suivante, nous pouvons effectuer une vérification, qui peut entraîner une réponse asynchrone.
  duplicateEntry = someCheckHere();
  if (duplicateEntry === true) {
    deferred = $.Deferred();
    // ici, nous lançons une autre fonction (par exemple $.ajax or popup window) 
    // qui en fonction du résultat doit appeler deferred.resolve([opt args - response])
    // lorsque deferred.resolve est appelé, "asyncLoop" démarrera une nouvelle itération
    // exemple de fonction :
    exampleFunction(duplicateEntry, deferred);
    return deferred;
  } else {
    return someActionIfNotDuplicate();
  }
};

exemple de fonction qui résout deferred:

function exampleFunction(entry, deffered){
  openModal({
    title: "Qu'est-ce que nous devrions faire avec le doublon",
    options: [
       {name:"Remplacer", action: function(){replace(entry);deffered.resolve(replace:true)}},
       {name: "Conserver l'existant", action: function(){deffered.resolve(replace:false)}}
    ]
  })
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X