Como fazer uma função esperar até que um retorno de chamada seja chamado usando node.js


266

Eu tenho uma função simplificada que se parece com isso:

function(query) {
  myApi.exec('SomeCommand', function(response) {
    return response;
  });
}

Basicamente, eu quero chamar myApi.exec, e retornar a resposta que é dada no lambda de retorno de chamada. No entanto, o código acima não funciona e simplesmente retorna imediatamente.

Apenas por uma tentativa muito tola, tentei o abaixo que não funcionou, mas pelo menos você entendeu o que estou tentando alcançar:

function(query) {
  var r;
  myApi.exec('SomeCommand', function(response) {
    r = response;
  });
  while (!r) {}
  return r;
}

Basicamente, qual é uma boa maneira 'node.js / event driven' de fazer isso? Quero que minha função aguarde até que o retorno de chamada seja chamado e retorne o valor que foi passado para ele.


3
Ou estou fazendo isso da maneira errada aqui e devo ligar para outro retorno de chamada, em vez de retornar uma resposta?
22411 Chris

Esta é, na minha opinião, a melhor explicação para o SO, por que o loop ocupado não funciona.
bluenote10

Não tente esperar. Basta ligar para próxima função (dependente do retorno de chamada) dentro de no final de si callback
Atul

Respostas:


282

A maneira "bom node.js / event driven" de fazer isso é não esperar .

Como quase todo o resto ao trabalhar com sistemas controlados por eventos como o nó, sua função deve aceitar um parâmetro de retorno de chamada que será chamado quando o cálculo for concluído. O chamador não deve esperar que o valor seja "retornado" no sentido normal, mas sim enviar a rotina que manipulará o valor resultante:

function(query, callback) {
  myApi.exec('SomeCommand', function(response) {
    // other stuff here...
    // bla bla..
    callback(response); // this will "return" your value to the original caller
  });
}

Então você não o usa assim:

var returnValue = myFunction(query);

Mas assim:

myFunction(query, function(returnValue) {
  // use the return value here instead of like a regular (non-evented) return value
});

5
OK ótimo. E se o myApi.exec nunca chamar o retorno de chamada? Como eu faria para que o retorno de chamada seja chamado após, digamos, 10 segundos com um valor de erro dizendo que cronometrou nossa ou algo assim?
22411 Chris

5
Ou melhor ainda (adicionou-se uma verificação de modo a chamada de retorno não pode ser invocado duas vezes): jsfiddle.net/LdaFw/1
Jakob

148
É claro que não-bloqueio é o padrão em node / js, no entanto, certamente há momentos em que o bloqueio é desejado (por exemplo, bloqueio em stdin). O nó par possui métodos de "bloqueio" (consulte todos os fs sync*métodos). Como tal, acho que ainda é uma pergunta válida. Existe uma boa maneira de conseguir o bloqueio no nó, além da espera ocupada?
Nategood 12/05/12

7
Uma resposta tardia ao comentário de @nategood: posso pensar em algumas maneiras; muito para explicar neste comentário, mas pesquise no Google. Lembre-se de que o Nó não foi criado para ser bloqueado, portanto, eles não são perfeitos. Pense nelas como sugestões. De qualquer forma, aqui está: (1) Use C para implementar sua função e publique-a no NPM para usá-la. É isso que os syncmétodos fazem. (2) Use fibras, github.com/laverdet/node-fibers , (3) Use promessas, por exemplo, a biblioteca Q, (4) Use uma camada fina em cima do javascript, que parece estar bloqueando, mas compila para assíncrona, como maxtaco.github.com/coffee-script
Jakob

106
É tão frustrante quando as pessoas respondem a uma pergunta com "você não deve fazer isso". Se alguém quiser ser útil e responder a uma pergunta, é uma coisa de pé. Mas dizer-me inequivocamente que não devo fazer algo é apenas hostil. Há um milhão de razões diferentes pelas quais alguém deseja chamar uma rotina de forma síncrona ou assíncrona. Esta foi uma pergunta sobre como fazê-lo. Se você fornecer conselhos úteis sobre a natureza da API enquanto fornece a resposta, isso é útil, mas se você não fornecer uma resposta, por que se incomodar em responder. (Eu acho que eu realmente deve dirigir meu próprio conselho.)
Howard Swope

46

Uma maneira de conseguir isso é agrupar a chamada da API em uma promessa e usá-la awaitpara aguardar o resultado.

// let's say this is the API function with two callbacks,
// one for success and the other for error
function apiFunction(query, successCallback, errorCallback) {
    if (query == "bad query") {
        errorCallback("problem with the query");
    }
    successCallback("Your query was <" + query + ">");
}

// myFunction wraps the above API call into a Promise
// and handles the callbacks with resolve and reject
function apiFunctionWrapper(query) {
    return new Promise((resolve, reject) => {
        apiFunction(query,(successResponse) => {
            resolve(successResponse);
        }, (errorResponse) => {
            reject(errorResponse)
        });
    });
}

// now you can use await to get the result from the wrapped api function
// and you can use standard try-catch to handle the errors
async function businessLogic() {
    try {
        const result = await apiFunctionWrapper("query all users");
        console.log(result);

        // the next line will fail
        const result2 = await apiFunctionWrapper("bad query");
    } catch(error) {
        console.error("ERROR:" + error);
    }
}

// call the main function
businessLogic();

Resultado:

Your query was <query all users>
ERROR:problem with the query

Este é um exemplo muito bem feito de agrupar uma função com um retorno de chamada para que você possa usá-la async/await . Muitas vezes não preciso disso, por isso não consigo lembrar de como lidar com essa situação. Estou copiando isso para minhas notas / referências pessoais.
robert arles


10

Se você não quiser usar a chamada de retorno, use o módulo "Q".

Por exemplo:

function getdb() {
    var deferred = Q.defer();
    MongoClient.connect(databaseUrl, function(err, db) {
        if (err) {
            console.log("Problem connecting database");
            deferred.reject(new Error(err));
        } else {
            var collection = db.collection("url");
            deferred.resolve(collection);
        }
    });
    return deferred.promise;
}


getdb().then(function(collection) {
   // This function will be called afte getdb() will be executed. 

}).fail(function(err){
    // If Error accrued. 

});

Para mais informações, consulte: https://github.com/kriskowal/q


9

Se você deseja que seja muito simples e fácil, sem bibliotecas sofisticadas, esperar que as funções de retorno de chamada sejam executadas no nó, antes de executar outro código, é assim:

//initialize a global var to control the callback state
var callbackCount = 0;
//call the function that has a callback
someObj.executeCallback(function () {
    callbackCount++;
    runOtherCode();
});
someObj2.executeCallback(function () {
    callbackCount++;
    runOtherCode();
});

//call function that has to wait
continueExec();

function continueExec() {
    //here is the trick, wait until var callbackCount is set number of callback functions
    if (callbackCount < 2) {
        setTimeout(continueExec, 1000);
        return;
    }
    //Finally, do what you need
    doSomeThing();
}

5

Nota: Essa resposta provavelmente não deve ser usada no código de produção. É um truque e você deve saber sobre as implicações.

Existe o módulo uvrun (atualizado para as versões mais recentes do Nodejs aqui ) onde você pode executar uma única rodada de loop do loop do evento principal do libuv (que é o loop principal do Nodejs).

Seu código ficaria assim:

function(query) {
  var r;
  myApi.exec('SomeCommand', function(response) {
    r = response;
  });
  var uvrun = require("uvrun");
  while (!r)
    uvrun.runOnce();
  return r;
}

(Você pode usar um método alternativo uvrun.runNoWait(). Isso pode evitar alguns problemas com o bloqueio, mas requer 100% da CPU.)

Observe que essa abordagem invalida todo o objetivo do Nodejs, ou seja, ter tudo assíncrono e sem bloqueio. Além disso, isso pode aumentar muito a profundidade do seu pilha de chamadas, para que você possa acabar com estouros de pilha. Se você executar essa função recursivamente, definitivamente encontrará problemas.

Veja as outras respostas sobre como redesenhar seu código para fazê-lo "corretamente".

Esta solução aqui provavelmente é útil apenas quando você faz testes e esp. deseja ter código sincronizado e serial.


5

Desde o nó 4.8.0, você pode usar o recurso do ES6 chamado gerador. Você pode seguir este artigo para obter conceitos mais aprofundados. Mas basicamente você pode usar geradores e promessas para fazer esse trabalho. Estou usando o bluebird para promisificar e gerenciar o gerador.

Seu código deve estar bem, como no exemplo abaixo.

const Promise = require('bluebird');

function* getResponse(query) {
  const r = yield new Promise(resolve => myApi.exec('SomeCommand', resolve);
  return r;
}

Promise.coroutine(getResponse)()
  .then(response => console.log(response));

1

supondo que você tenha uma função:

var fetchPage(page, callback) {
   ....
   request(uri, function (error, response, body) {
        ....
        if (something_good) {
          callback(true, page+1);
        } else {
          callback(false);
        }
        .....
   });


};

você pode usar retornos de chamada como este:

fetchPage(1, x = function(next, page) {
if (next) {
    console.log("^^^ CALLBACK -->  fetchPage: " + page);
    fetchPage(page, x);
}
});

-1

Isso anula o objetivo de não bloquear IO - você está bloqueando quando não precisa de bloqueio :)

Você deve aninhar seus retornos de chamada em vez de forçar o node.js a aguardar ou chamar outro retorno de chamada dentro do retorno de chamada em que precisa do resultado r.

As chances são de que, se você precisar forçar o bloqueio, está pensando em sua arquitetura errada.


Eu suspeitava que isso acontecesse ao contrário.
22411 Chris

31
Provavelmente, eu só quero escrever um script rápido para http.get()algum URL e console.log()seu conteúdo. Por que eu tenho que pular para trás para fazer isso no Node?
Dan Dascalescu

6
@ DanDascalescu: E por que tenho que declarar assinaturas de tipo para fazê-lo em idiomas estáticos? E por que eu tenho que colocá-lo no método principal em linguagens do tipo C? E por que eu tenho que compilá-lo em um idioma compilado? O que você está questionando é uma decisão de design fundamental no Node.js. Essa decisão tem prós e contras. Se você não gostar, poderá usar outro idioma que se adapte melhor ao seu estilo. É por isso que temos mais de um.
21414 Jakob

@ Jakob: as soluções que você listou são realmente ótimas. Isso não significa que não há bons, como o uso de Node no servidor pelo Meteor em fibras, o que elimina o problema do inferno de retorno de chamada.
Dan Dascalescu

13
@ Jakob: Se a melhor resposta para "por que o ecossistema X torna a tarefa comum Y desnecessariamente difícil?" é "se você não gosta, não use o ecossistema X", isso é um forte sinal de que os designers e mantenedores do ecossistema X estão priorizando seus próprios egos acima da usabilidade real do ecossistema. Tem sido minha experiência que a comunidade Node (em contraste com as comunidades Ruby, Elixir e até PHP) se esforça para dificultar tarefas comuns. Muito obrigado por se oferecer como um exemplo vivo desse antipadrão.
Jazz

-1

Usar async e aguardar é muito mais fácil.

router.post('/login',async (req, res, next) => {
i = await queries.checkUser(req.body);
console.log('i: '+JSON.stringify(i));
});

//User Available Check
async function checkUser(request) {
try {
    let response = await sql.query('select * from login where email = ?', 
    [request.email]);
    return response[0];

    } catch (err) {
    console.log(err);

  }

}

A API usada na pergunta não retorna uma promessa, então você precisaria enviá-la primeiro ... como esta resposta fez dois anos atrás.
Quentin
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.