Il modo più semplice per attendere il completamento di alcune attività asincrone, in Javascript?


112

Voglio eliminare alcune raccolte mongodb, ma è un'attività asincrona. Il codice sarà:

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 visualizza:

all dropped
dropped
dropped
dropped

Qual è il modo più semplice per assicurarti all droppedche venga stampato dopo che tutte le raccolte sono state eliminate? Qualsiasi terza parte può essere utilizzata per semplificare il codice.

Risposte:


92

Vedo che stai usando mongoosequindi stai parlando di JavaScript lato server. In tal caso consiglio di guardare il modulo asincrono e utilizzare async.parallel(...). Troverai questo modulo davvero utile: è stato sviluppato per risolvere il problema con cui stai lottando. Il tuo codice potrebbe essere simile a questo

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

Con questo ... il metodo forEach avviene in modo asincrono. Quindi, se l'elenco degli oggetti fosse più lungo dei 3 descritti qui, non potrebbe essere il caso che quando viene valutata async.parallel (calls, function (err, result) le chiamate non contengono ancora tutte le funzioni nell'elenco originale?
Martin Beeby,

5
@MartinBeeby forEachè sincrono. Dai un'occhiata qui: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… C'è l'implementazione di forEachin fondo. Non tutto con la richiamata è asincrono.
bizzarro

2
Per la cronaca, async può essere utilizzato anche in un browser.
Erwin Wessels

@MartinBeeby Tutto ciò con un callback È asincrono, il problema è che a forEach non viene passato un "callback", ma solo una normale funzione (che è un uso errato della terminologia da parte di Mozilla). In un linguaggio di programmazione funzionale, non chiameresti mai una funzione passata come "richiamata"

3
@ ghert85 No, non c'è niente di sbagliato nella terminologia. Il callback è semplicemente qualsiasi codice eseguibile passato come argomento ad altro codice e che dovrebbe essere eseguito a un certo punto. Questa è la definizione standard. E può essere chiamato in modo sincrono o asincrono. Vedi questo: en.wikipedia.org/wiki/Callback_(computer_programming)
bizzarro

128

Usa le promesse .

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

Questa operazione elimina ogni raccolta, stampa "eliminata" dopo ciascuna e quindi stampa "tutte le raccolte" al termine. Se si verifica un errore, viene visualizzato stderr.


Risposta precedente (questa è precedente al supporto nativo di Node per Promises):

Usa le promesse Q o le promesse Bluebird .

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

Con 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
Le promesse sono la strada da percorrere. Bluebird è un'altra libreria di promesse che funzionerebbe bene se fosse in codice critico per le prestazioni. Dovrebbe essere un sostituto immediato. Basta usare require('bluebird').
weiyin

Ho aggiunto un esempio Bluebird. È un po 'diverso poiché il modo migliore per usare Bluebird è usare la promisifyAllfunzione.
Nate

Qualche idea su come funziona promisifyAll .. Ho letto documenti ma non capisco è che il modo in cui gestisce funzioni che non piacciono ai parametri function abc(data){, perché non è come function abc(err, callback){...Fondamentalmente non penso che tutte le funzioni prendano l'errore come primo parametro e callback come secondo parametro
Muhammad Umer

@MuhammadUmer Molti dettagli su bluebirdjs.com/docs/api/promise.promisifyall.html
Nate

È passato un po 'di tempo da quando il driver MongoDB supporta anche le promesse. Puoi aggiornare il tuo esempio per trarne vantaggio? .map(function(name) { return conn.collection(name).drop() })
djanowski

21

Il modo per farlo è passare alle attività un callback che aggiorna un contatore condiviso. Quando il contatore condiviso raggiunge lo zero, sai che tutte le attività sono terminate in modo da poter continuare con il tuo flusso normale.

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

Naturalmente, ci sono molti modi per rendere questo tipo di codice più generico o riutilizzabile e qualsiasi delle tante librerie di programmazione asincrona là fuori dovrebbe avere almeno una funzione per fare questo genere di cose.


Questo potrebbe non essere il più semplice da implementare, ma mi piace molto vedere una risposta che non richiede moduli esterni. Grazie!
controbeing

8

Espandendo la risposta @freakish, async offre anche un metodo each, che sembra particolarmente adatto al tuo caso:

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

IMHO, questo rende il codice più efficiente e più leggibile. Mi sono preso la libertà di rimuovere il console.log('dropped')- se lo desideri, usa questo invece:

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

Lo faccio senza biblioteche esterne:

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

Tutte le risposte sono piuttosto vecchie. Dall'inizio del 2013 Mongoose ha iniziato a supportare le promesse gradualmente per tutte le query, quindi questo sarebbe il modo consigliato per strutturare diverse chiamate asincrone nell'ordine richiesto, immagino.


0

Con deferred(un'altra promessa / implementazione differita) puoi fare:

// 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

Se stai usando Babel o simili transpilers e stai usando async / await potresti fare:

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

Non puoi passare una richiamata drop()e aspettarti di restituire una promessa. Puoi correggere questo esempio e rimuoverlo onDrop?
djanowski
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.