Come far attendere una funzione fino a quando non viene chiamato un callback usando node.js


266

Ho una funzione semplificata che assomiglia a questa:

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

Fondamentalmente voglio che chiami myApi.exece restituisca la risposta che è data nel callback lambda. Tuttavia, il codice sopra non funziona e restituisce semplicemente immediatamente.

Solo per un tentativo molto hacker, ho provato il seguito che non ha funzionato, ma almeno hai l'idea di cosa sto cercando di ottenere:

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

Fondamentalmente, qual è un buon modo 'node.js / event driven' per farlo? Voglio che la mia funzione attenda fino a quando non viene chiamato il callback, quindi restituisce il valore che gli è stato passato.


3
O sto andando completamente nel modo sbagliato qui, e dovrei chiamare un altro callback, piuttosto che restituire una risposta?
Chris,

Questa è secondo me la migliore spiegazione SO del perché il ciclo di occupato non funziona.
bluenote10,

Non provare ad aspettare. Basta chiamare la funzione successiva (dipendente dalla richiamata) entro alla fine della richiamata stessa
Atul

Risposte:


282

Il modo "good node.js / event driven" per farlo è di non aspettare .

Come quasi tutto il resto quando si lavora con sistemi basati su eventi come il nodo, la funzione deve accettare un parametro di callback che verrà richiamato quando il calcolo è completo. Il chiamante non dovrebbe attendere che il valore venga "restituito" in senso normale, ma piuttosto inviare la routine che gestirà il valore risultante:

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

Quindi non lo usi in questo modo:

var returnValue = myFunction(query);

Ma così:

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

5
Ok fantastico. Che dire se myApi.exec non ha mai chiamato il callback? Come lo farei in modo che il callback venga chiamato dopo dire 10 secondi con un valore di errore che dice che ha cronometrato il nostro o qualcosa del genere?
Chris,

5
O meglio ancora (aggiunto un controllo in modo che il callback non possa essere invocato due volte): jsfiddle.net/LdaFw/1
Jakob

148
È chiaro che il non-blocco è lo standard in node / js, tuttavia ci sono certamente momenti in cui si desidera il blocco (ad es. Blocco su stdin). Anche il nodo ha metodi di "blocco" (vedi tutti i fs sync*metodi). In quanto tale, penso che questa sia ancora una domanda valida. C'è un bel modo per ottenere il blocco nel nodo oltre all'attesa occupata?
nategood,

7
Una risposta tardiva al commento di @nategood: posso pensare a un paio di modi; troppo da spiegare in questo commento, ma cercali su Google. Ricorda che il nodo non è fatto per essere bloccato, quindi questi non sono perfetti. Pensa a loro come suggerimenti. Ad ogni modo, ecco qui: (1) Usa C per implementare la tua funzione e pubblicarla su NPM per usarla. Ecco cosa fanno i syncmetodi. (2) Usa fibre, github.com/laverdet/node-fibers , (3) Usa le promesse, ad esempio la Q-library, (4) Usa un sottile strato sopra javascript, che sembra bloccante, ma si compila in modo da sincronizzarsi, come maxtaco.github.com/coffee-script
Jakob

106
È così frustrante quando le persone rispondono a una domanda con "non dovresti farlo". Se uno vuole essere utile e rispondere a una domanda, questa è una cosa da fare. Ma dirmi inequivocabilmente che non dovrei fare qualcosa è solo ostile. Ci sono un milione di ragioni diverse per cui qualcuno vorrebbe chiamare una routine in modo sincrono o asincrono. Questa era una domanda su come farlo. Se fornisci consigli utili sulla natura dell'api mentre fornisci la risposta, questo è utile, ma se non fornisci una risposta, perché preoccuparsi di rispondere. (Immagino che dovrei davvero seguire il mio consiglio.)
Howard Swope,

46

Un modo per raggiungere questo obiettivo è avvolgere la chiamata API in una promessa e quindi utilizzare awaitper attendere il risultato.

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

Produzione:

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

Questo è un esempio molto ben fatto di come avvolgere una funzione con un callback in modo da poterlo usare con async/await non ho spesso bisogno di questo, quindi ho difficoltà a ricordare come gestire questa situazione, sto copiando questo per le mie note / riferimenti personali.
Robert Arles,


10

Se non si desidera utilizzare la richiamata, è possibile utilizzare il modulo "Q".

Per esempio:

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. 

});

Per ulteriori informazioni, consultare: https://github.com/kriskowal/q


9

Se vuoi che sia molto semplice e facile, niente librerie fantasiose, attendere che le funzioni di callback vengano eseguite nel nodo, prima di eseguire qualche altro codice, è così:

//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: questa risposta probabilmente non dovrebbe essere utilizzata nel codice di produzione. È un trucco e dovresti conoscere le implicazioni.

C'è l'uvrun modulo (aggiornato per le versioni più recenti Nodejs qui ), dove è possibile eseguire un singolo round ciclo della libuv ciclo evento principale (che è la Nodejs loop principale).

Il tuo codice sarebbe simile al seguente:

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

(Potrebbe essere un uso alternativo uvrun.runNoWait(). Ciò potrebbe evitare alcuni problemi con il blocco, ma richiede il 100% di CPU.)

Si noti che questo tipo di approccio invalida l'intero scopo di Nodejs, ovvero avere tutto asincrono e non bloccante. Inoltre, potrebbe aumentare molto la profondità del tuo callstack, quindi potresti finire con overflow dello stack. Se si esegue tale funzione in modo ricorsivo, si verificheranno sicuramente problemi.

Vedi le altre risposte su come riprogettare il codice per farlo "giusto".

Questa soluzione qui è probabilmente utile solo quando si eseguono test ed esp. desidera avere un codice seriale e sincronizzato.


5

Dal nodo 4.8.0 è possibile utilizzare la funzione di ES6 chiamata generatore. Puoi seguire questo articolo per concetti più profondi. Ma fondamentalmente puoi usare generatori e promesse per fare questo lavoro. Sto usando bluebird per promuovere e gestire il generatore.

Il tuo codice dovrebbe andare bene come nell'esempio seguente.

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

supponendo che tu abbia una funzione:

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


};

puoi utilizzare callback come questo:

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

-1

Ciò vanifica lo scopo dell'IO non bloccante: lo stai bloccando quando non ha bisogno di essere bloccato :)

È necessario nidificare i callback invece di forzare node.js ad attendere oppure chiamare un altro callback all'interno del callback da cui è necessario il risultato r.

È probabile che, se devi forzare il blocco, stai pensando alla tua architettura in modo sbagliato.


Avevo il sospetto di averlo fatto al contrario.
Chris,

31
È probabile, voglio solo scrivere uno script veloce su http.get()alcuni URL e il console.log()suo contenuto. Perché devo saltare indietro per farlo in Nodo?
Dan Dascalescu il

6
@DanDascalescu: E perché devo dichiarare le firme di tipo per farlo in linguaggi statici? E perché devo metterlo in un metodo principale in linguaggi simili al C? E perché devo compilarlo in un linguaggio compilato? Quello che stai mettendo in discussione è una decisione di progettazione fondamentale in Node.js. Tale decisione presenta pro e contro. Se non ti piace, puoi usare un'altra lingua che si adatta meglio al tuo stile. Ecco perché ne abbiamo più di uno.
Jakob,

@Jakob: le soluzioni che hai elencato sono davvero non ottimali. Ciò non significa che non ce ne siano di buoni, come l'uso da parte di Meteor di Node nelle fibre, che elimina il problema dell'inferno di callback.
Dan Dascalescu,

13
@Jakob: se la migliore risposta a "perché l'ecosistema X rende inutilmente difficile il compito comune Y?" è "se non ti piace, non usare l'ecosistema X", allora questo è un forte segnale che i progettisti e i manutentori dell'ecosistema X stanno dando la priorità al proprio ego rispetto alla reale fruibilità del loro ecosistema. È stata la mia esperienza che la community Node (in contrasto con le community Ruby, Elixir e persino PHP) fa di tutto per rendere difficili le attività comuni. Grazie mille per offrirti come esempio vivente di quell'antipasto.
Jazz,

-1

L'uso di asincrono e l'attesa è molto più semplice.

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

  }

}

L'API utilizzata nella domanda non restituisce una promessa, quindi dovresti racchiuderla in una prima ... come questa risposta ha fatto due anni fa.
Quentin,
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.