So lassen Sie eine Funktion mit node.js warten, bis ein Rückruf aufgerufen wurde


266

Ich habe eine vereinfachte Funktion, die so aussieht:

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

Grundsätzlich möchte ich, dass es aufruft myApi.execund die Antwort zurückgibt, die im Rückruf-Lambda gegeben wird. Der obige Code funktioniert jedoch nicht und kehrt einfach sofort zurück.

Nur für einen sehr hackigen Versuch habe ich das Folgende ausprobiert, was nicht funktioniert hat, aber zumindest kommt Ihnen die Idee, was ich erreichen möchte:

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

Was ist im Grunde eine gute "node.js / event-gesteuerte" Vorgehensweise? Ich möchte, dass meine Funktion wartet, bis der Rückruf aufgerufen wird, und dann den Wert zurückgibt, der an sie übergeben wurde.


3
Oder gehe ich hier völlig falsch vor und sollte ich einen anderen Rückruf anrufen, anstatt eine Antwort zurückzugeben?
Chris

Dies ist meiner Meinung nach die beste SO-Erklärung, warum die Besetztschleife nicht funktioniert.
bluenote10

Versuche nicht zu warten. Rufen Sie einfach die nächste Funktion (rückrufabhängig) am Ende des Rückrufs selbst auf
Atul

Antworten:


282

Die "gute node.js / eventgesteuerte" Methode, dies zu tun, besteht darin, nicht zu warten .

Wie fast alles andere bei der Arbeit mit ereignisgesteuerten Systemen wie Node sollte Ihre Funktion einen Rückrufparameter akzeptieren, der aufgerufen wird, wenn die Berechnung abgeschlossen ist. Der Aufrufer sollte nicht warten, bis der Wert im normalen Sinne "zurückgegeben" wird, sondern die Routine senden, die den resultierenden Wert verarbeitet:

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

Also benutzt du es nicht so:

var returnValue = myFunction(query);

Aber so:

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

5
Ok toll. Was ist, wenn myApi.exec den Rückruf nie aufgerufen hat? Wie würde ich es schaffen, dass der Rückruf nach etwa 10 Sekunden mit einem Fehlerwert aufgerufen wird, der angibt, dass er zeitlich festgelegt ist oder so?
Chris

5
Oder noch besser (ein Scheck wurde hinzugefügt, damit der Rückruf nicht zweimal aufgerufen werden kann): jsfiddle.net/LdaFw/1
Jakob

148
Es ist klar, dass Nicht-Blockieren der Standard in Knoten / js ist, aber es gibt sicherlich Zeiten, in denen Blockieren gewünscht wird (z. B. Blockieren auf Standard). Sogar der Knoten hat "blockierende" Methoden (siehe alle fs sync*Methoden). Insofern halte ich dies immer noch für eine berechtigte Frage. Gibt es eine gute Möglichkeit, das Blockieren im Knoten zu erreichen, abgesehen vom geschäftigen Warten?
Nategood

7
Eine späte Antwort auf den Kommentar von @nategood: Ich kann mir ein paar Möglichkeiten vorstellen; zu viel, um es in diesem Kommentar zu erklären, aber google sie. Denken Sie daran, dass der Knoten nicht zum Blockieren vorgesehen ist, sodass diese nicht perfekt sind. Betrachten Sie sie als Vorschläge. Wie auch immer, hier ist: (1) Verwenden Sie C, um Ihre Funktion zu implementieren und sie in NPM zu veröffentlichen, um sie zu verwenden. Das machen die syncMethoden. (2) Verwenden Sie Fasern, github.com/laverdet/node-fibers . (3) Verwenden Sie Versprechen, z. B. die Q-Bibliothek. (4) Verwenden Sie eine dünne Schicht über Javascript, die blockiert aussieht, aber asynchron kompiliert wird. wie maxtaco.github.com/coffee-script
Jakob

106
Es ist so frustrierend, wenn Leute eine Frage mit "Das solltest du nicht tun" beantworten. Wenn man hilfreich sein und eine Frage beantworten möchte, ist das eine gute Sache. Aber mir unmissverständlich zu sagen, dass ich nichts tun soll, ist einfach unfreundlich. Es gibt eine Million verschiedene Gründe, warum jemand eine Routine synchron oder asynchron aufrufen möchte. Dies war eine Frage, wie es geht. Wenn Sie hilfreiche Ratschläge zur Art der API geben, während Sie die Antwort geben, ist dies hilfreich. Wenn Sie jedoch keine Antwort geben, warum sollten Sie dann antworten? (Ich denke, ich sollte wirklich meinen eigenen Rat befolgen.)
Howard Swope

45

Eine Möglichkeit, dies zu erreichen, besteht darin, den API-Aufruf in ein Versprechen zu verpacken und dann awaitauf das Ergebnis zu warten.

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

Ausgabe:

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

Dies ist ein sehr gelungenes Beispiel für das Umschließen einer Funktion mit einem Rückruf, damit Sie sie verwenden können. async/await Ich brauche dies nicht oft. Sie haben also Probleme, sich daran zu erinnern, wie Sie mit dieser Situation umgehen sollen. Ich kopiere dies für meine persönlichen Notizen / Referenzen.
Robert Arles


10

Wenn Sie den Rückruf nicht verwenden möchten, können Sie das Modul "Q" verwenden.

Beispielsweise:

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. 

});

Weitere Informationen finden Sie unter: https://github.com/kriskowal/q


9

Wenn Sie möchten, dass es sehr einfach und unkompliziert ist, warten Sie auf die Ausführung von Rückruffunktionen im Knoten, bevor Sie einen anderen Code ausführen:

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

Hinweis: Diese Antwort sollte wahrscheinlich nicht im Produktionscode verwendet werden. Es ist ein Hack und Sie sollten über die Auswirkungen Bescheid wissen.

Es ist das uvrun Modul (aktualisiert für neuere Versionen NodeJS hier ) , wo man eine einzige Schleife Runde der libuv Hauptereignisschleife ausführen kann (die die NodeJS Hauptschleife ist).

Ihr Code würde folgendermaßen aussehen:

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

(Sie könnten eine alternative Verwendung verwenden uvrun.runNoWait(). Dadurch könnten einige Probleme beim Blockieren vermieden werden, es wird jedoch 100% CPU benötigt.)

Beachten Sie, dass dieser Ansatz den gesamten Zweck von Nodejs ungültig macht, dh alles asynchron und nicht blockierend zu machen. Außerdem kann dies die Callstack-Tiefe erheblich erhöhen, sodass möglicherweise Stapelüberläufe auftreten. Wenn Sie eine solche Funktion rekursiv ausführen, werden Sie definitiv auf Probleme stoßen.

In den anderen Antworten erfahren Sie, wie Sie Ihren Code neu gestalten, um ihn "richtig" zu machen.

Diese Lösung hier ist wahrscheinlich nur nützlich, wenn Sie testen und esp. möchte synchronisierten und seriellen Code haben.


5

Seit Knoten 4.8.0 können Sie die als Generator bezeichnete Funktion von ES6 verwenden. Sie können diesem Artikel für tiefere Konzepte folgen . Aber im Grunde können Sie Generatoren und Versprechen verwenden, um diese Arbeit zu erledigen. Ich benutze Bluebird , um den Generator zu versprechen und zu verwalten.

Ihr Code sollte wie im folgenden Beispiel in Ordnung sein.

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

Angenommen, Sie haben eine Funktion:

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


};

Sie können Rückrufe wie folgt verwenden:

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

-1

Das macht den Zweck des nicht blockierenden E / A zunichte - Sie blockieren es, wenn es nicht blockiert werden muss :)

Sie sollten Ihre Rückrufe verschachteln, anstatt node.js zum Warten zu zwingen, oder einen anderen Rückruf innerhalb des Rückrufs aufrufen, bei dem Sie das Ergebnis von benötigen r.

Wenn Sie das Blockieren erzwingen müssen, denken Sie wahrscheinlich an Ihre Architektur.


Ich hatte den Verdacht, dass ich das rückwärts hatte.
Chris

31
Wahrscheinlich möchte ich nur ein kurzes Skript für http.get()eine URL und console.log()deren Inhalt schreiben . Warum muss ich rückwärts springen, um das in Node zu tun?
Dan Dascalescu

6
@DanDascalescu: Und warum muss ich Typensignaturen deklarieren, um dies in statischen Sprachen zu tun? Und warum muss ich es in eine Hauptmethode in C-ähnlichen Sprachen setzen? Und warum muss ich es in einer kompilierten Sprache kompilieren? Was Sie in Frage stellen, ist eine grundlegende Entwurfsentscheidung in Node.js. Diese Entscheidung hat Vor- und Nachteile. Wenn es Ihnen nicht gefällt, können Sie eine andere Sprache verwenden, die besser zu Ihrem Stil passt. Deshalb haben wir mehr als eine.
Jakob

@ Jakob: Die Lösungen, die Sie aufgelistet haben, sind in der Tat suboptimal. Das bedeutet nicht, dass es keine guten gibt, wie zum Beispiel die serverseitige Verwendung von Node in Fasern durch Meteor, wodurch das Problem der Rückrufhölle beseitigt wird.
Dan Dascalescu

13
@ Jakob: Wenn die beste Antwort auf "Warum macht Ökosystem X die gemeinsame Aufgabe Y unnötig schwierig?" ist "Wenn es Ihnen nicht gefällt, verwenden Sie Ökosystem X nicht", dann ist dies ein starkes Zeichen dafür, dass die Designer und Betreuer von Ökosystem X ihr eigenes Ego vor der tatsächlichen Nutzbarkeit ihres Ökosystems priorisieren. Ich habe die Erfahrung gemacht, dass die Node-Community (im Gegensatz zu den Ruby-, Elixir- und sogar PHP-Communitys) alles daran setzt, gemeinsame Aufgaben zu erschweren. Vielen Dank, dass Sie sich als lebendiges Beispiel für dieses Antimuster anbieten.
Jazz

-1

Mit Async und Warten ist es viel einfacher.

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

  }

}

Die in der Frage verwendete API gibt kein Versprechen zurück, daher müssten Sie sie zuerst in eine einschließen ... wie diese Antwort vor zwei Jahren.
Quentin
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.