¿La forma más sencilla de esperar que se completen algunas tareas asincrónicas, en Javascript?

112

Quiero eliminar algunas colecciones de mongodb, pero esa es una tarea asincrónica. El código será:

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 consola muestra:

all dropped
dropped
dropped
dropped

¿Cuál es la forma más sencilla de asegurarse de all droppedque se imprima después de que se hayan eliminado todas las colecciones? Se puede utilizar cualquier tercero para simplificar el código.

Freewind
fuente

Respuestas:

92

Veo que está utilizando, mongoosepor lo que está hablando de JavaScript del lado del servidor. En ese caso, aconsejo mirar el módulo async y usar async.parallel(...). Este módulo le resultará realmente útil: fue desarrollado para resolver el problema con el que está luchando. Tu código puede verse así

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);
});
monstruoso
fuente
Con esto ... el método forEach es asincrónico. Entonces, si la lista de objetos fuera más larga que los 3 detallados aquí, ¿no podría ser el caso que cuando se evalúa async.parallel (llamadas, función (err, resultado), las llamadas aún no contienen todas las funciones en la lista original?
Martin Beeby
5
@MartinBeeby forEaches sincrónico. Eche un vistazo aquí: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Hay una implementación de forEachen la parte inferior. No todo con devolución de llamada es asincrónico.
extraño
2
Para el registro, async también se puede utilizar en un navegador.
Erwin Wessels
@MartinBeeby Todo con una devolución de llamada ES asincrónico, el problema es que a forEach no se le está pasando una "devolución de llamada", sino solo una función normal (que es un uso incorrecto de la terminología de Mozilla). En un lenguaje de programación funcional, nunca llamarías a una función pasada una "devolución de llamada"
3
@ ghert85 No, no hay nada de malo en la terminología. La devolución de llamada es simplemente cualquier código ejecutable que se pasa como argumento a otro código y se espera que se ejecute en algún momento. Esa es la definición estándar. Y se puede llamar de forma sincrónica o asincrónica. Vea esto: en.wikipedia.org/wiki/Callback_(computer_programming)
freakish
128

Utilice promesas .

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

Esto descarta cada colección, imprimiendo "descartadas" después de cada una y luego imprime "todas descartadas" cuando se completa. Si ocurre un error, se muestra a stderr.


Respuesta anterior (esto es anterior al soporte nativo de Node para Promises):

Use promesas Q o promesas 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);
Nate
fuente
1
Las promesas son el camino a seguir. Bluebird es otra biblioteca de promesas que funcionaría bien si se tratara de un código de rendimiento crítico. Debería ser un reemplazo directo. Solo usa require('bluebird').
Weiyin
Agregué un ejemplo de Bluebird. Es un poco diferente ya que la mejor manera de usar Bluebird es usar la promisifyAllfunción.
Nate
Cualquier idea de cómo funciona promisifyAll ... He leído documentos pero no lo entiendo es cómo maneja las funciones que no tienen parámetros como function abc(data){, porque no es como function abc(err, callback){...Básicamente, no creo que todas las funciones tomen el error como primer parámetro y la devolución de llamada como segundo parámetro
Muhammad Umer
@MuhammadUmer Muchos detalles en bluebirdjs.com/docs/api/promise.promisifyall.html
Nate
Ha pasado un tiempo desde que el controlador MongoDB también admite promesas. ¿Puede actualizar su ejemplo para aprovechar esto? .map(function(name) { return conn.collection(name).drop() })
djanowski
21

La forma de hacerlo es pasar a las tareas una devolución de llamada que actualiza un contador compartido. Cuando el contador compartido llegue a cero, sabrá que todas las tareas han finalizado para que pueda continuar con su flujo 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);

Por supuesto, hay muchas formas de hacer que este tipo de código sea más genérico o reutilizable y cualquiera de las muchas bibliotecas de programación asíncrona que existen debería tener al menos una función para hacer este tipo de cosas.

hugomg
fuente
Puede que esto no sea el más fácil de implementar, pero realmente me gusta ver una respuesta que no requiere módulos externos. ¡Gracias!
contraataque el
8

Ampliando la respuesta @freakish, async también ofrece cada método, que parece especialmente adecuado para su 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');
});

En mi humilde opinión, esto hace que el código sea más eficiente y más legible. Me he tomado la libertad de eliminar el console.log('dropped'), si lo desea, use esto en su lugar:

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');
});
Erwin Wessels
fuente
5

Hago esto sin bibliotecas externas:

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

Todas las respuestas son bastante antiguas. Desde principios de 2013, Mongoose comenzó a admitir promesas gradualmente para todas las consultas, por lo que supongo que esa sería la forma recomendada de estructurar varias llamadas asíncronas en el orden requerido en el futuro.

Capaj
fuente
0

Con deferred(otra promesa / implementación diferida) puede hacer:

// 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);
Mariusz Nowak
fuente
0

Si está usando Babel o tales transpilers y usa async / await, puede hacer:

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");
}
ganaraj
fuente
No puede pasar una devolución de llamada drop()y esperar devolver una Promesa. ¿Puedes arreglar este ejemplo y eliminarlo onDrop?
djanowski