Jak sprawić, aby funkcja czekała na wywołanie zwrotne za pomocą node.js


266

Mam uproszczoną funkcję, która wygląda następująco:

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

Zasadniczo chcę, aby zadzwonił myApi.execi zwrócił odpowiedź podaną w wywołaniu zwrotnym lambda. Jednak powyższy kod nie działa i po prostu zwraca natychmiast.

Tylko dla bardzo hackerskiej próby wypróbowałem poniższe, które nie działały, ale przynajmniej masz pojęcie, co próbuję osiągnąć:

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

Zasadniczo, co jest dobrym sposobem na osiągnięcie tego? Chcę, aby moja funkcja czekała na wywołanie wywołania zwrotnego, a następnie zwraca wartość, która została do niej przekazana.


3
Czy też robię to w zupełnie niewłaściwy sposób i czy powinienem wywoływać kolejne oddzwanianie, zamiast zwracać odpowiedź?
Chris

To jest moim zdaniem najlepsze wytłumaczenie SO, dlaczego pętla zajętości nie działa.
bluenote 10

Nie próbuj czekać. Wystarczy wywołać następną funkcję (zależną od oddzwaniania) na końcu samego oddzwaniania
Atul

Odpowiedzi:


282

„Dobrym sterowaniem opartym na zdarzeniach node.js / event” jest nie czekać .

Podobnie jak prawie wszystko inne podczas pracy z systemami sterowanymi zdarzeniami, takimi jak węzeł, twoja funkcja powinna zaakceptować parametr wywołania zwrotnego, który zostanie wywołany po zakończeniu obliczeń. Osoba dzwoniąca nie powinna czekać na „zwrócenie” wartości w normalnym sensie, ale raczej wysłać procedurę, która będzie obsługiwała wynikową wartość:

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

Więc nie używaj go w ten sposób:

var returnValue = myFunction(query);

Ale tak:

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

5
Ok świetnie. Co jeśli myApi.exec nigdy nie oddzwoni? Jak mam to zrobić, aby oddzwanianie było wywoływane po powiedzmy 10 sekundach z wartością błędu informującą o tym, że czas nasz lub coś takiego?
Chris

5
Lub jeszcze lepiej (dodano czek, aby wywołanie zwrotne nie mogło
Jakob

148
Oczywiste jest, że nieblokowanie jest standardem w węźle / js, jednak z pewnością są chwile, kiedy pożądane jest blokowanie (np. Blokowanie na standardowe wejście). Nawet węzeł ma metody „blokowania” (zobacz wszystkie fs sync*metody). W związku z tym uważam, że to wciąż ważne pytanie. Czy jest jakiś dobry sposób na uzyskanie blokowania w węźle oprócz zajętego oczekiwania?
nategood

7
Późna odpowiedź na komentarz @nategood: Mogę wymyślić kilka sposobów; zbyt wiele, by wyjaśnić w tym komentarzu, ale google. Pamiętaj, że Węzeł nie jest blokowany, więc nie są idealne. Pomyśl o nich jak o sugestiach. W każdym razie tutaj: (1) Użyj C, aby zaimplementować swoją funkcję i opublikuj ją w NPM, aby z niej skorzystać. Tak robią syncmetody. (2) Użyj włókien, github.com/laverdet/node-fibers , (3) Użyj obietnic, na przykład biblioteki Q, (4) Użyj cienkiej warstwy na javascript, która wygląda na blokującą, ale kompiluje się w celu asynchronizacji, jak maxtaco.github.com/coffee-script
Jakob

106
To frustrujące, gdy ludzie odpowiadają na pytanie „nie powinieneś tego robić”. Jeśli ktoś chce być pomocny i odpowiedzieć na pytanie, to jest wstanie. Ale powiedzenie mi jednoznacznie, że nie powinienem robić czegoś, jest po prostu nieprzyjazne. Istnieje milion różnych powodów, dla których ktoś chciałby wywołać procedurę synchronicznie lub asynchronicznie. To było pytanie, jak to zrobić. Jeśli podczas udzielania odpowiedzi udzielasz przydatnych porad na temat natury interfejsu API, jest to pomocne, ale jeśli nie podajesz odpowiedzi, po co zawracać sobie głowę odpowiedzią. (Chyba powinienem naprawdę kierować się własnymi radami.)
Howard Swope

45

Jednym ze sposobów osiągnięcia tego jest zawinięcie wywołania interfejsu API w obietnicę, a następnie użycie go w awaitcelu oczekiwania na wynik.

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

Wynik:

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

Jest to bardzo dobrze wykonany przykład owijania funkcji wywołaniem zwrotnym, aby można było z niej korzystać. async/await Nie potrzebuję tego często, więc mam problemy z zapamiętywaniem, jak sobie z tym poradzić, kopiuję to dla moich osobistych notatek / referencji.
Robert Arles


10

Jeśli nie chcesz korzystać z oddzwaniania, możesz użyć modułu „Q”.

Na przykład:

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. 

});

Aby uzyskać więcej informacji, patrz: https://github.com/kriskowal/q


9

Jeśli chcesz, aby było to bardzo proste i łatwe, bez fantazyjnych bibliotek, czekanie na wykonanie funkcji zwrotnych w węźle przed wykonaniem innego kodu wygląda następująco:

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

Uwaga: prawdopodobnie nie należy używać tej odpowiedzi w kodzie produkcyjnym. To hack i powinieneś wiedzieć o implikacjach.

Istnieje uvrun moduł (zaktualizowane do nowszych wersji Nodejs tutaj ), gdzie można wykonać jedną rundę pętli na libuv głównej pętli zdarzeń (co jest Nodejs głównym pętla).

Twój kod wyglądałby tak:

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

(Możesz użyć alternatywnie uvrun.runNoWait(). Pozwoli to uniknąć problemów z blokowaniem, ale zajmie 100% procesora.)

Zauważ, że takie podejście unieważnia cały cel Nodejsa, tj. Aby wszystko było asynchroniczne i nieblokujące. Może to również znacznie zwiększyć głębokość stosu wywołań, co może spowodować przepełnienie stosu. Jeśli uruchomisz taką funkcję rekurencyjnie, na pewno napotkasz problemy.

Zobacz inne odpowiedzi na temat przeprojektowania kodu, aby zrobić to „dobrze”.

To rozwiązanie tutaj jest prawdopodobnie przydatne tylko podczas testowania i esp. chcę mieć zsynchronizowany i seryjny kod.


5

Od węzła 4.8.0 możesz korzystać z funkcji ES6 zwanej generatorem. Możesz zapoznać się z tym artykułem, aby uzyskać więcej informacji. Ale w zasadzie możesz użyć generatorów i obietnic, aby wykonać tę pracę. Używam Bluebird do promisify i zarządzać generator.

Twój kod powinien być w porządku, tak jak w przykładzie poniżej.

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

zakładając, że masz funkcję:

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


};

możesz skorzystać z takich wywołań zwrotnych:

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

-1

To przeczy celowi nieblokowania IO - blokujesz go, gdy nie wymaga blokowania :)

Powinieneś zagnieździć swoje wywołania zwrotne zamiast zmuszać node.js do czekania lub wywołać inne wywołanie zwrotne wewnątrz wywołania zwrotnego, w którym potrzebujesz wyniku r.

Możliwe, że jeśli musisz wymusić blokowanie, źle myślisz o swojej architekturze.


Miałem podejrzenie, że miałem to odwrócone.
Chris

31
Możliwe, że po prostu chcę napisać szybki skrypt do http.get()jakiegoś adresu URL i console.log()jego zawartości. Dlaczego muszę przeskakiwać wstecz, aby to zrobić w Węźle?
Dan Dascalescu

6
@DanDascalescu: I dlaczego muszę zadeklarować podpisy typu, aby zrobić to w językach statycznych? I dlaczego muszę umieścić to w głównej metodzie w językach podobnych do C? I dlaczego muszę go skompilować w skompilowanym języku? To, co kwestionujesz, jest podstawową decyzją projektową w Node.js. Ta decyzja ma zalety i wady. Jeśli ci się nie podoba, możesz użyć innego języka, który lepiej pasuje do Twojego stylu. Właśnie dlatego mamy więcej niż jeden.
Jakob,

@Jakob: wymienione przez Ciebie rozwiązania są naprawdę nieoptymalne. To nie znaczy, że nie ma dobrych, takich jak użycie przez Meteor węzła we włóknach po stronie serwera, co eliminuje problem z piekłem zwrotnym.
Dan Dascalescu,

13
@Jakob: Jeśli najlepsza odpowiedź na pytanie „dlaczego ekosystem X sprawia, że ​​wspólne zadanie Y jest niepotrzebnie trudne?” to „jeśli ci się nie podoba, nie używaj ekosystemu X”, to jest to mocny znak, że projektanci i opiekunowie ekosystemu X nadają priorytet swoim własnym ego ponad faktyczną użyteczność ich ekosystemu. Z mojego doświadczenia wynika, że ​​społeczność Node (w przeciwieństwie do społeczności Ruby, Elixir, a nawet społeczności PHP) robi wszystko, aby utrudniać wykonywanie typowych zadań. Dziękuję TAK DUŻO za zaoferowanie siebie jako żywego przykładu tego antypatternu.
Jazz

-1

Korzystanie z asynchronizacji i czekania jest o wiele łatwiejsze.

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

  }

}

Interfejs API użyty w pytaniu nie zwraca obietnicy, więc musisz najpierw go zawinąć… tak jak ta odpowiedź dwa lata temu.
Quentin
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.