Cara membuat fungsi menunggu hingga panggilan balik telah dipanggil menggunakan node.js

266

Saya memiliki fungsi sederhana yang terlihat seperti ini:

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

Pada dasarnya saya ingin menelepon myApi.exec, dan mengembalikan respons yang diberikan dalam lambda callback. Namun, kode di atas tidak berfungsi dan langsung kembali segera.

Hanya untuk upaya yang sangat kejam, saya mencoba di bawah ini yang tidak berhasil, tetapi setidaknya Anda mendapatkan ide apa yang saya coba capai:

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

Pada dasarnya, apa yang baik dari 'node.js / event driven' dalam hal ini? Saya ingin fungsi saya menunggu sampai panggilan balik dipanggil, lalu mengembalikan nilai yang diteruskan ke sana.

Chris
sumber
3
Atau apakah saya benar-benar salah tentang hal itu di sini, dan haruskah saya menelepon panggilan balik yang lain, daripada mengembalikan jawaban?
Chris
Ini menurut saya penjelasan SO terbaik mengapa loop sibuk tidak berfungsi.
bluenote10
Jangan mencoba menunggu. Panggil saja fungsi berikutnya (tergantung panggilan balik) di dalam pada akhir panggilan balik itu sendiri
Atul

Jawaban:

282

Cara "node.js / event driven" yang baik untuk melakukan ini adalah tidak menunggu .

Seperti hampir semua hal lain ketika bekerja dengan sistem yang digerakkan oleh peristiwa seperti node, fungsi Anda harus menerima parameter panggilan balik yang akan dipanggil saat perhitungan selesai. Penelepon tidak boleh menunggu sampai nilai "dikembalikan" dalam arti normal, tetapi mengirim rutin yang akan menangani nilai yang dihasilkan:

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

Jadi Anda tidak menggunakannya seperti ini:

var returnValue = myFunction(query);

Tapi seperti ini:

myFunction(query, function(returnValue) {
  // use the return value here instead of like a regular (non-evented) return value
});
Jakob
sumber
5
OK bagus. Bagaimana jika myApi.exec tidak pernah menelepon kembali? Bagaimana saya membuatnya sehingga panggilan balik dipanggil setelah mengatakan 10 detik dengan nilai kesalahan mengatakan itu waktunya kami atau sesuatu?
Chris
5
Atau lebih baik lagi (menambahkan cek sehingga callback tidak dapat dipanggil dua kali): jsfiddle.net/LdaFw/1
Jakob
148
Jelas non-blocking adalah standar dalam node / js, namun ada saat-saat tertentu yang diinginkan (misalnya blocking pada stdin). Bahkan node memiliki metode "pemblokiran" (lihat semua fs sync*metode). Karena itu, saya pikir ini masih pertanyaan yang valid. Apakah ada cara yang bagus untuk mencapai pemblokiran di simpul selain dari kesibukan menunggu?
nategood
7
Jawaban terlambat untuk komentar oleh @nategood: Saya bisa memikirkan beberapa cara; terlalu banyak untuk dijelaskan dalam komentar ini, tetapi google mereka. Ingat bahwa Node tidak dibuat untuk diblokir, jadi ini tidak sempurna. Pikirkan itu sebagai saran. Bagaimanapun, begini: (1) Gunakan C untuk mengimplementasikan fungsi Anda dan publikasikan ke NPM untuk menggunakannya. Itulah yang syncdilakukan metode. (2) Gunakan serat, github.com/laverdet/node-fibers , (3) Gunakan janji-janji, misalnya perpustakaan-Q, (4) Gunakan lapisan tipis di atas javascript, yang terlihat memblokir, tetapi kompilasi ke async, seperti maxtaco.github.com/coffee-script
Jakob
106
Sangat menyebalkan ketika orang menjawab pertanyaan dengan "Anda seharusnya tidak melakukan itu." Jika seseorang ingin membantu dan menjawab pertanyaan, itu adalah hal yang harus dilakukan. Tetapi dengan tegas mengatakan kepada saya bahwa saya seharusnya tidak melakukan sesuatu sama sekali tidak ramah. Ada sejuta alasan berbeda mengapa seseorang ingin memanggil rutin secara sinkron atau asinkron. Ini adalah pertanyaan tentang bagaimana melakukannya. Jika Anda memberikan saran bermanfaat tentang sifat api sambil memberikan jawaban, itu membantu, tetapi jika Anda tidak memberikan jawaban, mengapa repot-repot membalas. (Saya kira saya harus benar-benar mengindahkan saran saya sendiri.)
Howard Swope
46

Salah satu cara untuk mencapai ini adalah dengan membungkus panggilan API menjadi janji dan kemudian gunakan awaituntuk menunggu hasilnya.

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

Keluaran:

Your query was <query all users>
ERROR:problem with the query
Timo
sumber
Ini adalah contoh yang sangat baik dari membungkus suatu fungsi dengan panggilan balik sehingga Anda dapat menggunakannya dengan async/await Saya tidak sering membutuhkan ini, jadi mengalami kesulitan mengingat bagaimana menangani situasi ini, saya menyalin ini untuk catatan / referensi pribadi saya.
robert arles
10

Jika Anda tidak ingin menggunakan panggilan balik maka Anda dapat menggunakan modul "Q".

Sebagai contoh:

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. 

});

Untuk informasi lebih lanjut lihat ini: https://github.com/kriskowal/q

Pathal Vishal
sumber
9

Jika Anda menginginkannya sangat sederhana dan mudah, tanpa pustaka mewah, untuk menunggu fungsi callback dieksekusi dalam node, sebelum mengeksekusi beberapa kode lain, seperti ini:

//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();
}
Marquinho Peli
sumber
5

Catatan: Jawaban ini mungkin sebaiknya tidak digunakan dalam kode produksi. Ini hack dan Anda harus tahu tentang implikasinya.

Ada modul uvrun (diperbarui untuk versi Nodejs yang lebih baru di sini ) di mana Anda dapat mengeksekusi satu putaran putaran libuv loop peristiwa utama (yang merupakan loop utama Nodejs).

Kode Anda akan terlihat seperti ini:

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

(Anda mungkin menggunakan alternatif uvrun.runNoWait(). Itu bisa menghindari beberapa masalah dengan pemblokiran, tetapi membutuhkan CPU 100%.)

Perhatikan bahwa pendekatan semacam ini membatalkan seluruh tujuan Nodejs, yaitu memiliki segala sesuatu yang async dan non-blocking. Selain itu, ini bisa meningkatkan kedalaman callstack Anda, sehingga Anda mungkin berakhir dengan stack overflow. Jika Anda menjalankan fungsi tersebut secara rekursif, Anda pasti akan mengalami masalah.

Lihat jawaban lain tentang cara mendesain ulang kode Anda untuk melakukannya "benar".

Solusi ini di sini mungkin hanya berguna ketika Anda melakukan pengujian dan esp. ingin disinkronkan dan kode seri.

Albert
sumber
5

Karena node 4.8.0 Anda dapat menggunakan fitur ES6 yang disebut generator. Anda dapat mengikuti artikel ini untuk konsep yang lebih dalam. Tetapi pada dasarnya Anda dapat menggunakan generator dan janji untuk menyelesaikan pekerjaan ini. Saya menggunakan bluebird untuk menjanjikan dan mengelola generator.

Kode Anda harus baik seperti contoh di bawah ini.

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));
Douglas Soares
sumber
1

seandainya Anda memiliki fungsi:

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


};

Anda dapat memanfaatkan panggilan balik seperti ini:

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

Itu mengalahkan tujuan non-blocking IO - Anda memblokirnya saat tidak perlu diblokir :)

Anda harus membuat sarang panggilan balik Anda alih-alih memaksa node.js menunggu, atau menelepon panggilan balik lain di dalam panggilan balik di mana Anda memerlukan hasil r.

Kemungkinannya adalah, jika Anda perlu memaksa pemblokiran, Anda berpikir bahwa arsitektur Anda salah.


sumber
Saya curiga saya memiliki ini di belakang.
Chris
31
Kemungkinannya adalah, saya hanya ingin menulis skrip cepat ke http.get()beberapa URL dan console.log()isinya. Mengapa saya harus melompat mundur untuk melakukannya di Node?
Dan Dascalescu
6
@DanDascalescu: Dan mengapa saya harus menyatakan tanda tangan jenis untuk melakukannya dalam bahasa statis? Dan mengapa saya harus meletakkannya di metode utama dalam bahasa mirip C? Dan mengapa saya harus mengkompilasinya dalam bahasa yang dikompilasi? Apa yang Anda tanyakan adalah keputusan desain mendasar di Node.js. Keputusan itu memiliki pro dan kontra. Jika Anda tidak menyukainya, Anda dapat menggunakan bahasa lain yang lebih sesuai dengan gaya Anda. Itu sebabnya kami memiliki lebih dari satu.
Jakob
@ Jakob: solusi yang Anda daftarkan memang suboptimal. Itu tidak berarti tidak ada yang bagus, seperti server Node menggunakan serat di sisi server, yang menghilangkan masalah neraka panggilan balik.
Dan Dascalescu
13
@ Jakob: Jika jawaban terbaik untuk "mengapa ekosistem X membuat tugas bersama Y menjadi sulit?" adalah "jika Anda tidak menyukainya, jangan gunakan ekosistem X," maka itu adalah tanda kuat bahwa perancang dan pemelihara ekosistem X memprioritaskan ego mereka sendiri di atas kegunaan aktual ekosistem mereka. Sudah pengalaman saya bahwa komunitas Node (berbeda dengan komunitas Ruby, Elixir, dan bahkan PHP) keluar dari jalannya untuk membuat tugas-tugas umum menjadi sulit. Terima kasih BANYAK untuk menawarkan diri Anda sebagai contoh hidup dari antipattern itu.
Jazz
-1

Menggunakan async dan menunggu itu jauh lebih mudah.

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

  }

}
SaiSurya
sumber
API yang digunakan dalam pertanyaan tidak mengembalikan janji, jadi Anda harus membungkusnya dengan yang pertama ... seperti jawaban ini lakukan dua tahun lalu.
Quentin