Maneira mais simples de esperar algumas tarefas assíncronas concluídas, em Javascript?


112

Eu quero descartar algumas coleções mongodb, mas essa é uma tarefa assíncrona. O 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');

O console exibe:

all dropped
dropped
dropped
dropped

Qual é a maneira mais simples de garantir que all droppedserão impressas depois que todas as coleções forem descartadas? Qualquer terceiro pode ser usado para simplificar o código.

Respostas:


92

Vejo que você está usando, mongooseentão, está falando sobre JavaScript do lado do servidor. Nesse caso, aconselho olhar para o módulo assíncrono e usar async.parallel(...). Você achará este módulo realmente útil - ele foi desenvolvido para resolver o problema com o qual você está lutando. Seu código pode ser assim

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

Com isso ... o método forEach acontece de forma assíncrona. Portanto, se a lista de objetos fosse maior do que os 3 detalhados aqui, não poderia ser o caso de que quando async.parallel (chamadas, função (errar, resultado) fosse avaliada, as chamadas ainda não contivessem todas as funções na lista original?
Martin Beeby

5
@MartinBeeby forEaché síncrono. Dê uma olhada aqui: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Há implementação de forEachna parte inferior. Nem tudo com retorno de chamada é assíncrono.
bizarro

2
Para o registro, async também pode ser usado em um navegador.
Erwin Wessels

@MartinBeeby Tudo com um callback É assíncrono, o problema é que forEach não está sendo passado um "callback", mas apenas uma função regular (que é o uso incorreto da terminologia da Mozilla). Em uma linguagem de programação funcional, você nunca chamaria uma função passada de "retorno de chamada"

3
@ ghert85 Não, não há nada de errado com a terminologia. O retorno de chamada é simplesmente qualquer código executável que é passado como um argumento para outro código e deve ser executado em algum ponto. Essa é a definição padrão. E pode ser chamado de forma síncrona ou assíncrona. Veja isto: en.wikipedia.org/wiki/Callback_(computer_programming)
bizarro

128

Use promessas .

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

Isso elimina cada coleção, imprimindo “descartado” após cada uma e, em seguida, imprime “tudo descartado” quando concluído. Se ocorrer um erro, ele será exibido para stderr.


Resposta anterior (este suporte nativo do Node anterior a Promises):

Use promessas Q ou promessas Bluebird .

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

Com 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
As promessas são o caminho a percorrer. Bluebird é outra biblioteca promissora que funcionaria bem se fosse em um código de desempenho crítico. Deve ser uma substituição imediata. Basta usar require('bluebird').
weiyin 01 de

Eu adicionei um exemplo Bluebird. É um pouco diferente, pois a melhor maneira de usar o Bluebird é usando o promisifyAllrecurso.
Nate

Alguma ideia de como o promisifyAll funciona ... Eu li a documentação, mas não entendi, é como ele lida com funções que não possuem parâmetros como function abc(data){, porque não é como function abc(err, callback){...Basicamente, eu não acho que todas as funções levam o erro como primeiro parâmetro e callback como segundo parâmetro
Muhammad Umer

@MuhammadUmer Muitos detalhes em bluebirdjs.com/docs/api/promise.promisifyall.html
Nate

Já faz um tempo que o driver MongoDB também oferece suporte a promessas. Você pode atualizar seu exemplo para tirar vantagem disso? .map(function(name) { return conn.collection(name).drop() })
djanowski de

21

A maneira de fazer isso é passar às tarefas um retorno de chamada que atualiza um contador compartilhado. Quando o contador compartilhado chega a zero, você sabe que todas as tarefas foram concluídas, para que possa continuar com seu fluxo 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);

Claro, existem muitas maneiras de tornar esse tipo de código mais genérico ou reutilizável e qualquer uma das muitas bibliotecas de programação assíncrona por aí deve ter pelo menos uma função para fazer esse tipo de coisa.


Isso pode não ser o mais fácil de implementar, mas eu realmente gosto de ver uma resposta que não requer módulos externos. Obrigado!
contra-estar em

8

Expandindo a resposta @freakish, o async também oferece um método each, que parece especialmente adequado para o seu 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, isso torna o código mais eficiente e mais legível. Tomei a liberdade de remover o console.log('dropped')- se quiser, use o seguinte:

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

Eu faço isso sem 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');
        }
    });                
});

4

Todas as respostas são bastante antigas. Desde o início de 2013, o Mongoose começou a oferecer suporte a promessas gradualmente para todas as consultas, então essa seria a forma recomendada de estruturar várias chamadas assíncronas na ordem necessária daqui para frente, eu acho.


0

Com deferred(outra promessa / implementação adiada) você pode fazer:

// 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 você estiver usando Babel ou outros transpiladores e usando async / await, você pode fazer:

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

Você não pode passar um retorno de chamada drop()e esperar retornar uma promessa. Você pode corrigir este exemplo e remover onDrop?
djanowski de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.