Le moyen le plus simple d'attendre la fin de certaines tâches asynchrones, en Javascript?


112

Je veux supprimer certaines collections mongodb, mais c'est une tâche asynchrone. Le code sera:

var mongoose = require('mongoose');

mongoose.connect('mongo://localhost/xxx');

var conn = mongoose.connection;

['aaa','bbb','ccc'].forEach(function(name){
    conn.collection(name).drop(function(err) {
        console.log('dropped');
    });
});
console.log('all dropped');

La console affiche:

all dropped
dropped
dropped
dropped

Quelle est la manière la plus simple de s'assurer all droppedqu'elle sera imprimée une fois que toutes les collections auront été supprimées? N'importe quel tiers peut être utilisé pour simplifier le code.

Réponses:


92

Je vois que vous utilisez mongoosedonc vous parlez de JavaScript côté serveur. Dans ce cas, je conseille de regarder le module async et de l'utiliser async.parallel(...). Vous trouverez ce module très utile - il a été développé pour résoudre le problème avec lequel vous êtes aux prises. Votre code peut ressembler à ceci

var async = require('async');

var calls = [];

['aaa','bbb','ccc'].forEach(function(name){
    calls.push(function(callback) {
        conn.collection(name).drop(function(err) {
            if (err)
                return callback(err);
            console.log('dropped');
            callback(null, name);
        });
    }
)});

async.parallel(calls, function(err, result) {
    /* this code will run after all calls finished the job or
       when any of the calls passes an error */
    if (err)
        return console.log(err);
    console.log(result);
});

Avec ceci ... la méthode forEach se produit de manière asynchrone. Donc, si la liste d'objets était plus longue que les 3 détaillés ici, ne pourrait-il pas être le cas que lorsque async.parallel (appels, fonction (err, résultat) est évalué, les appels ne contiennent pas encore toutes les fonctions de la liste d'origine?
Martin Beeby le

5
@MartinBeeby forEachest synchrone. Jetez un oeil ici: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... Il y a une implémentation de forEachen bas. Tout avec le rappel n'est pas asynchrone.
freakish

2
Pour mémoire, async peut également être utilisé dans un navigateur.
Erwin Wessels

@MartinBeeby Tout avec un callback EST asynchrone, le problème est que forEach n'est pas passé un "callback", mais juste une fonction normale (ce qui est une mauvaise utilisation de la terminologie par Mozilla). Dans un langage de programmation fonctionnel, vous n'appelleriez jamais une fonction passée un "rappel"

3
@ ghert85 Non, il n'y a rien de mal avec la terminologie. Le rappel est simplement n'importe quel code exécutable qui est passé en argument à un autre code et qui devrait être exécuté à un moment donné. C'est la définition standard. Et il peut être appelé de manière synchrone ou asynchrone. Voir ceci: en.wikipedia.org/wiki/Callback_(computer_programming)
freakish

128

Utilisez des promesses .

var mongoose = require('mongoose');

mongoose.connect('your MongoDB connection string');
var conn = mongoose.connection;

var promises = ['aaa', 'bbb', 'ccc'].map(function(name) {
  return new Promise(function(resolve, reject) {
    var collection = conn.collection(name);
    collection.drop(function(err) {
      if (err) { return reject(err); }
      console.log('dropped ' + name);
      resolve();
    });
  });
});

Promise.all(promises)
.then(function() { console.log('all dropped)'); })
.catch(console.error);

Cela supprime chaque collection, l'impression «abandonnée» après chacune d'elles, puis imprime «tout abandonné» une fois terminé. Si une erreur se produit, elle s'affiche sous stderr.


Réponse précédente (ceci est antérieur au support natif de Node pour Promises):

Utilisez les promesses Q ou les promesses Bluebird .

Avec Q :

var Q = require('q');
var mongoose = require('mongoose');

mongoose.connect('your MongoDB connection string');
var conn = mongoose.connection;

var promises = ['aaa','bbb','ccc'].map(function(name){
    var collection = conn.collection(name);
    return Q.ninvoke(collection, 'drop')
      .then(function() { console.log('dropped ' + name); });
});

Q.all(promises)
.then(function() { console.log('all dropped'); })
.fail(console.error);

Avec Bluebird :

var Promise = require('bluebird');
var mongoose = Promise.promisifyAll(require('mongoose'));

mongoose.connect('your MongoDB connection string');
var conn = mongoose.connection;

var promises = ['aaa', 'bbb', 'ccc'].map(function(name) {
  return conn.collection(name).dropAsync().then(function() {
    console.log('dropped ' + name);
  });
});

Promise.all(promises)
.then(function() { console.log('all dropped'); })
.error(console.error);

1
Les promesses sont la voie à suivre. Bluebird est une autre bibliothèque de promesses qui fonctionnerait bien si elle était dans un code critique pour les performances. Ce devrait être un remplacement instantané. Utilisez simplement require('bluebird').
weiyin

J'ai ajouté un exemple Bluebird. C'est un peu différent puisque la meilleure façon d'utiliser Bluebird est d'utiliser la promisifyAllfonctionnalité.
Nate le

Toute idée de la façon dont promisifyAll fonctionne ... J'ai lu des documents mais je ne comprends pas que la façon dont il gère les fonctions qui ne sont pas des paramètres comme function abc(data){, parce que ce n'est pas comme function abc(err, callback){...Fondamentalement, je ne pense pas que toutes les fonctions prennent l'erreur comme premier paramètre et le rappel comme 2ème paramètre
Muhammad Umer

@MuhammadUmer Beaucoup de détails sur bluebirdjs.com/docs/api/promise.promisifyall.html
Nate

Cela fait un moment que le pilote MongoDB prend également en charge les promesses. Pouvez-vous mettre à jour votre exemple pour en profiter? .map(function(name) { return conn.collection(name).drop() })
djanowski le

21

La façon de le faire est de transmettre aux tâches un rappel qui met à jour un compteur partagé. Lorsque le compteur partagé atteint zéro, vous savez que toutes les tâches sont terminées afin que vous puissiez continuer votre flux normal.

var ntasks_left_to_go = 4;

var callback = function(){
    ntasks_left_to_go -= 1;
    if(ntasks_left_to_go <= 0){
         console.log('All tasks have completed. Do your stuff');
    }
}

task1(callback);
task2(callback);
task3(callback);
task4(callback);

Bien sûr, il existe de nombreuses façons de rendre ce type de code plus générique ou réutilisable et n'importe laquelle des nombreuses bibliothèques de programmation asynchrone devrait avoir au moins une fonction pour faire ce genre de chose.


Ce n'est peut-être pas la plus simple à mettre en œuvre, mais j'aime vraiment voir une réponse qui ne nécessite pas de modules externes. Je vous remercie!
contre-être

8

En développant la réponse @freakish, async propose également une méthode à chaque, qui semble particulièrement adaptée à votre cas:

var async = require('async');

async.each(['aaa','bbb','ccc'], function(name, callback) {
    conn.collection(name).drop( callback );
}, function(err) {
    if( err ) { return console.log(err); }
    console.log('all dropped');
});

À mon humble avis, cela rend le code à la fois plus efficace et plus lisible. J'ai pris la liberté de supprimer le console.log('dropped')- si vous le souhaitez, utilisez ceci à la place:

var async = require('async');

async.each(['aaa','bbb','ccc'], function(name, callback) {
    // if you really want the console.log( 'dropped' ),
    // replace the 'callback' here with an anonymous function
    conn.collection(name).drop( function(err) {
        if( err ) { return callback(err); }
        console.log('dropped');
        callback()
    });
}, function(err) {
    if( err ) { return console.log(err); }
    console.log('all dropped');
});

5

Je fais cela sans bibliothèques externes:

var yourArray = ['aaa','bbb','ccc'];
var counter = [];

yourArray.forEach(function(name){
    conn.collection(name).drop(function(err) {
        counter.push(true);
        console.log('dropped');
        if(counter.length === yourArray.length){
            console.log('all dropped');
        }
    });                
});

4

Toutes les réponses sont assez anciennes. Depuis le début de 2013, Mongoose a commencé à prendre en charge les promesses progressivement pour toutes les requêtes, ce serait donc la manière recommandée de structurer plusieurs appels asynchrones dans l'ordre requis à l'avenir, je suppose.


0

Avec deferred(une autre promesse / mise en œuvre différée), vous pouvez faire:

// Setup 'pdrop', promise version of 'drop' method
var deferred = require('deferred');
mongoose.Collection.prototype.pdrop =
    deferred.promisify(mongoose.Collection.prototype.drop);

// Drop collections:
deferred.map(['aaa','bbb','ccc'], function(name){
    return conn.collection(name).pdrop()(function () {
      console.log("dropped");
    });
}).end(function () {
    console.log("all dropped");
}, null);

0

Si vous utilisez Babel ou de tels transpilers et utilisez async / await, vous pouvez faire:

function onDrop() {
   console.log("dropped");
}

async function dropAll( collections ) {
   const drops = collections.map(col => conn.collection(col).drop(onDrop) );
   await drops;
   console.log("all dropped");
}

Vous ne pouvez pas transmettre un rappel drop()et vous attendre à renvoyer une promesse. Pouvez-vous s'il vous plaît corriger cet exemple et supprimer onDrop?
djanowski le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.