Panggil Fungsi Javascript Asinkron Secara Sinkron

222

Pertama, ini adalah kasus yang sangat spesifik untuk melakukannya dengan cara yang salah dengan sengaja untuk retrofit panggilan asinkron ke basis kode yang sangat sinkron yang ribuan baris panjang dan waktu saat ini tidak mampu melakukan perubahan untuk "melakukan itu benar." Itu menyakiti setiap serat dari keberadaan saya, tetapi kenyataan dan cita-cita sering tidak sesuai. Saya tahu ini menyebalkan.

OK, itu keluar dari jalan, bagaimana cara membuatnya sehingga saya bisa:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

  // block here and return data when the callback is finished
  return data;
}

Contoh-contoh (atau ketiadaan) semuanya menggunakan pustaka dan / atau kompiler, yang keduanya tidak layak untuk solusi ini. Saya perlu contoh konkret tentang cara membuatnya blok (misalnya TIDAK meninggalkan fungsi doSomething sampai panggilan balik dipanggil) TANPA membekukan UI. Jika hal seperti itu dimungkinkan di JS.

Robert C. Barth
sumber
16
Sama sekali tidak mungkin untuk membuat blok browser dan menunggu. Mereka tidak akan melakukannya.
Runcing
2
Dosis javascript memiliki mekanisme pemblokiran pada sebagian besar browser ... Anda ingin membuat panggilan balik yang dipanggil saat panggilan async selesai untuk mengembalikan data
Nadir Muzaffar
8
Anda meminta cara untuk memberi tahu browser "Saya tahu saya baru saja mengatakan kepada Anda untuk menjalankan fungsi sebelumnya secara tidak sinkron, tetapi saya tidak benar-benar bersungguh-sungguh!". Mengapa Anda bahkan berharap hal itu mungkin terjadi?
Wayne
2
Terima kasih Dan atas hasil editnya. Saya tidak sepenuhnya bersikap kasar, tetapi kata-kata Anda lebih baik.
Robert C. Barth
2
@ RobertC.Barth Sekarang juga dimungkinkan dengan JavaScript. async menunggu fungsi belum diratifikasi dalam standar, tetapi direncanakan akan di ES2017. Lihat jawaban saya di bawah ini untuk lebih detail.
John

Jawaban:

135

"jangan katakan padaku bagaimana aku harus melakukannya" dengan cara yang benar "atau apa pun"

BAIK. tetapi Anda harus benar-benar melakukannya dengan cara yang benar ... atau apa pun

"Aku butuh contoh konkret tentang cara membuatnya blok ... TANPA membekukan UI. Jika hal seperti itu mungkin terjadi di JS."

Tidak, tidak mungkin untuk memblokir JavaScript yang berjalan tanpa memblokir UI.

Mengingat kurangnya informasi, sulit untuk menawarkan solusi, tetapi salah satu opsi mungkin memiliki fungsi panggilan melakukan beberapa polling untuk memeriksa variabel global, kemudian mengatur callback datake global.

function doSomething() {

      // callback sets the received data to a global var
  function callBack(d) {
      window.data = d;
  }
      // start the async
  myAsynchronousCall(param1, callBack);

}

  // start the function
doSomething();

  // make sure the global is clear
window.data = null

  // start polling at an interval until the data is found at the global
var intvl = setInterval(function() {
    if (window.data) { 
        clearInterval(intvl);
        console.log(data);
    }
}, 100);

Semua ini mengasumsikan bahwa Anda dapat memodifikasi doSomething() . Saya tidak tahu apakah itu ada dalam kartu.

Jika itu dapat dimodifikasi, maka saya tidak tahu mengapa Anda tidak hanya meneruskan panggilan balik untuk doSomething()dipanggil dari panggilan balik lain, tapi saya lebih baik berhenti sebelum saya mendapat masalah. ;)


Oh, apa-apaan ini. Anda memberi contoh yang menunjukkan itu bisa dilakukan dengan benar, jadi saya akan menunjukkan solusi itu ...

function doSomething( func ) {

  function callBack(d) {
    func( d );
  }

  myAsynchronousCall(param1, callBack);

}

doSomething(function(data) {
    console.log(data);
});

Karena contoh Anda menyertakan panggilan balik yang diteruskan ke panggilan async, cara yang benar adalah dengan melewatkan fungsi yang doSomething()akan dipanggil dari panggilan balik.

Tentu saja jika itu satu-satunya hal yang dilakukan callback, Anda hanya akan lulus funclangsung ...

myAsynchronousCall(param1, func);
pengguna1106925
sumber
22
Ya, saya tahu bagaimana melakukannya dengan benar, saya perlu tahu bagaimana / jika itu dapat dilakukan dengan salah karena alasan spesifik yang dinyatakan. Intinya adalah saya tidak ingin meninggalkan doSomething () sampai myAsynchronousCall menyelesaikan panggilan ke fungsi callback. Bleh, itu tidak bisa dilakukan, seperti yang saya duga, saya hanya membutuhkan kebijaksanaan yang dikumpulkan dari Internet untuk mendukung saya. Terima kasih. :-)
Robert C. Barth
2
@ RobertC.Barth: Ya, kecurigaan Anda ternyata benar.
Apakah ini saya atau hanya versi "selesai dengan benar" yang berfungsi? Pertanyaannya termasuk panggilan balik, yang sebelumnya harus ada sesuatu yang menunggu panggilan async untuk selesai, yang bagian pertama dari jawaban ini tidak mencakup ...
ravemir
@ravemir: Jawabannya menyatakan bahwa tidak mungkin melakukan apa yang diinginkannya. Itulah bagian penting untuk dipahami. Dengan kata lain, Anda tidak dapat membuat panggilan tidak sinkron dan mengembalikan nilai tanpa memblokir UI. Jadi solusi pertama adalah hack jelek menggunakan variabel global dan polling untuk melihat apakah variabel itu telah dimodifikasi. Versi kedua adalah cara yang benar.
1
@Leonardo: Ini adalah fungsi misterius yang dipanggil dalam pertanyaan. Pada dasarnya itu mewakili apa pun yang menjalankan kode secara tidak sinkron dan menghasilkan hasil yang perlu diterima. Jadi itu bisa seperti permintaan AJAX. Anda meneruskan callbackfungsi ke myAsynchronousCallfungsi, yang melakukan hal-hal async dan memanggil kembali ketika selesai. Ini demo.
60

Fungsi Async , fitur di ES2017 , membuat sinkronisasi kode async dengan menggunakan janji (bentuk tertentu dari kode async) dan awaitkata kunci. Perhatikan juga dalam contoh kode di bawah kata kunci asyncdi depan functionkata kunci yang menandakan fungsi async / menunggu. Kata awaitkunci tidak akan berfungsi tanpa berada dalam fungsi yang telah diperbaiki sebelumnya dengan asynckata kunci. Karena saat ini tidak ada pengecualian untuk ini yang berarti tidak ada level atas yang menunggu akan berfungsi (level atas menunggu artinya menunggu di luar fungsi apa pun). Padahal ada proposal untuk level atasawait .

ES2017 telah disahkan (yaitu difinalisasi) sebagai standar untuk JavaScript pada 27 Juni 2017. Async menunggu mungkin sudah berfungsi di browser Anda, tetapi jika tidak Anda masih dapat menggunakan fungsionalitas menggunakan javascript transpiler seperti babel atau traceur . Chrome 55 mendapat dukungan penuh dari fungsi-fungsi async. Jadi, jika Anda memiliki peramban yang lebih baru, Anda mungkin dapat mencoba kode di bawah ini.

Lihat tabel kompatibilitas es2017 kangax untuk kompatibilitas peramban.

Berikut ini contoh fungsi menunggu async doAsyncyang dipanggil yang mengambil tiga satu jeda detik dan mencetak perbedaan waktu setelah setiap jeda dari waktu mulai:

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

async function doAsync () {
  var start = Date.now(), time;
  console.log(0);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
}

doAsync();

Ketika kata kunci yang menunggu ditempatkan sebelum nilai janji (dalam hal ini nilai janji adalah nilai yang dikembalikan oleh fungsi doSomethingAsync) kata kunci yang menunggu akan menjeda pelaksanaan panggilan fungsi, tetapi tidak akan menghentikan sementara fungsi lain dan itu akan melanjutkan mengeksekusi kode lain sampai janji terselesaikan. Setelah janji terselesaikan, itu akan membuka nilai janji dan Anda dapat memikirkan ekspresi menunggu dan janji yang sekarang sedang digantikan oleh nilai yang terbuka.

Jadi, karena menunggu hanya jeda menunggu kemudian membuka nilai sebelum mengeksekusi sisa baris Anda dapat menggunakannya dalam untuk loop dan panggilan fungsi di dalam seperti dalam contoh di bawah ini yang mengumpulkan perbedaan waktu yang ditunggu dalam array dan mencetak array.

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this calls each promise returning function one after the other
async function doAsync () {
  var response = [];
  var start = Date.now();
  // each index is a promise returning function
  var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync];
  for(var i = 0; i < promiseFuncs.length; ++i) {
    var promiseFunc = promiseFuncs[i];
    response.push(await promiseFunc() - start);
    console.log(response);
  }
  // do something with response which is an array of values that were from resolved promises.
  return response
}

doAsync().then(function (response) {
  console.log(response)
})

Fungsi async sendiri mengembalikan janji sehingga Anda dapat menggunakannya sebagai janji dengan rantai seperti yang saya lakukan di atas atau di dalam fungsi menunggu async lainnya.

Fungsi di atas akan menunggu setiap respons sebelum mengirim permintaan lain jika Anda ingin mengirim permintaan secara bersamaan, Anda dapat menggunakan Promise.all .

// no change
function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

// no change
function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this function calls the async promise returning functions all at around the same time
async function doAsync () {
  var start = Date.now();
  // we are now using promise all to await all promises to settle
  var responses = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);
  return responses.map(x=>x-start);
}

// no change
doAsync().then(function (response) {
  console.log(response)
})

Jika janji itu mungkin ditolak, Anda dapat membungkusnya dengan try catch atau melewati try catch dan membiarkan kesalahan tersebut menyebar ke fungsi async / await catch call. Anda harus berhati-hati untuk tidak meninggalkan kesalahan janji tidak tertangani terutama di Node.js. Di bawah ini adalah beberapa contoh yang menunjukkan bagaimana kesalahan bekerja.

function timeoutReject (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject(new Error("OOPS well you got an error at TIMESTAMP: " + Date.now()));
    }, time)
  })
}

function doErrorAsync () {
  return timeoutReject(1000);
}

var log = (...args)=>console.log(...args);
var logErr = (...args)=>console.error(...args);

async function unpropogatedError () {
  // promise is not awaited or returned so it does not propogate the error
  doErrorAsync();
  return "finished unpropogatedError successfully";
}

unpropogatedError().then(log).catch(logErr)

async function handledError () {
  var start = Date.now();
  try {
    console.log((await doErrorAsync()) - start);
    console.log("past error");
  } catch (e) {
    console.log("in catch we handled the error");
  }
  
  return "finished handledError successfully";
}

handledError().then(log).catch(logErr)

// example of how error propogates to chained catch method
async function propogatedError () {
  var start = Date.now();
  var time = await doErrorAsync() - start;
  console.log(time - start);
  return "finished propogatedError successfully";
}

// this is what prints propogatedError's error.
propogatedError().then(log).catch(logErr)

Jika Anda pergi ke sini, Anda dapat melihat proposal yang sudah jadi untuk versi ECMAScript mendatang.

Alternatif untuk ini yang dapat digunakan hanya dengan ES2015 (ES6) adalah dengan menggunakan fungsi khusus yang membungkus fungsi generator. Fungsi generator memiliki kata kunci hasil yang dapat digunakan untuk mereplikasi kata kunci yang menunggu dengan fungsi sekitarnya. Fungsi kata kunci dan generator hasil adalah tujuan yang jauh lebih umum dan dapat melakukan banyak hal lebih banyak daripada apa fungsi fungsi async menunggu. Jika Anda ingin pembungkus fungsi generator yang dapat digunakan untuk mereplikasi async, saya akan menunggu co.js . Ngomong-ngomong fungsi co sama seperti async menunggu fungsi mengembalikan janji. Jujur meskipun pada titik ini kompatibilitas browser hampir sama untuk fungsi generator dan fungsi async jadi jika Anda hanya ingin fungsi menunggu async Anda harus menggunakan fungsi Async tanpa co.js.

Dukungan browser sebenarnya cukup bagus sekarang untuk fungsi Async (pada 2017) di semua browser utama saat ini (Chrome, Safari, dan Edge) kecuali IE.

John
sumber
2
Saya suka jawaban ini
ycomp
1
seberapa jauh kita sudah datang :)
Derek
3
Ini adalah jawaban yang bagus, tetapi untuk masalah poster asli, saya pikir yang dilakukannya hanyalah memindahkan masalahnya ke atas satu tingkat. Katakanlah dia mengubah doSomething menjadi fungsi async dengan menunggu di dalam. Fungsi itu sekarang mengembalikan janji dan tidak sinkron, jadi dia harus berurusan dengan masalah yang sama lagi dalam apa pun panggilan fungsi itu.
dpwrussell
1
@dpwrussell ini benar, ada creep dari fungsi dan janji async di basis kode. Cara terbaik untuk menyelesaikan janji-janji dari merangkak masuk ke semuanya adalah dengan menulis panggilan balik yang sinkron, tidak ada cara untuk mengembalikan nilai async secara sinkron kecuali Anda melakukan sesuatu yang sangat aneh dan kontroversial seperti twitter.com/sebmarkbage/status/941214259505119232 ini yang tidak saya lakukan. sarankan. Saya akan menambahkan suntingan di akhir pertanyaan untuk lebih menjawab pertanyaan seperti yang ditanyakan dan bukan hanya menjawab judul.
John
Ini adalah jawaban yang bagus +1 dan semuanya, tetapi ditulis seperti ini, saya tidak melihat bagaimana ini tidak lebih rumit daripada menggunakan callback.
Altimus Prime
47

Lihatlah JQuery Promises:

http://api.jquery.com/promise/

http://api.jquery.com/jQuery.when/

http://api.jquery.com/deferred.promise/

Perbaiki kode:

    var dfd = jQuery.Deferred baru ();


    function callBack (data) {
       dfd.notify (data);
    }

    // lakukan panggilan async.
    myAsynchronousCall (param1, callBack);

    function doSomething (data) {
     // lakukan hal-hal dengan data ...
    }

    $ .when (dfd) .then (doSomething);


Matt Taylor
sumber
3
+1 untuk jawaban ini, ini benar. Namun, saya akan memperbarui baris dengan dfd.notify(data)untukdfd.resolve(data)
Jason
7
Apakah ini kasus kode yang memberikan ilusi sinkron, tanpa sebenarnya TIDAK sinkron?
saurshaz
2
janji adalah panggilan balik IMO terorganisir dengan baik :) jika Anda memerlukan panggilan asinkron dalam katakanlah beberapa inisialisasi objek, daripada janji membuat sedikit perbedaan.
webduvet
10
Janji tidak sinkron.
Vans S
6

Ada satu solusi yang bagus di http://taskjs.org/

Menggunakan generator yang baru mengenal javascript. Jadi saat ini tidak diterapkan oleh sebagian besar browser. Saya mengujinya di firefox, dan bagi saya itu cara yang bagus untuk membungkus fungsi asinkron.

Berikut ini contoh kode dari proyek GitHub

var { Deferred } = task;

spawn(function() {
    out.innerHTML = "reading...\n";
    try {
        var d = yield read("read.html");
        alert(d.responseText.length);
    } catch (e) {
        e.stack.split(/\n/).forEach(function(line) { console.log(line) });
        console.log("");
        out.innerHTML = "error: " + e;
    }

});

function read(url, method) {
    method = method || "GET";
    var xhr = new XMLHttpRequest();
    var deferred = new Deferred();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status >= 400) {
                var e = new Error(xhr.statusText);
                e.status = xhr.status;
                deferred.reject(e);
            } else {
                deferred.resolve({
                    responseText: xhr.responseText
                });
            }
        }
    };
    xhr.open(method, url, true);
    xhr.send();
    return deferred.promise;
}
George Vinokhodov
sumber
3

Anda dapat memaksa JavaScript asinkron di NodeJS agar sinkron dengan sync-rpc .

Ini pasti akan membekukan UI Anda, jadi saya masih tidak yakin ketika menyangkut apakah mungkin untuk mengambil jalan pintas yang perlu Anda ambil. Tidak mungkin untuk menangguhkan Utas Satu-Satunya dalam JavaScript, bahkan jika NodeJS memungkinkan Anda memblokirnya terkadang. Tidak ada panggilan balik, acara, apa pun yang tidak sinkron sama sekali akan dapat diproses sampai janji Anda terselesaikan. Jadi kecuali Anda pembaca memiliki situasi yang tidak dapat dihindari seperti OP (atau, dalam kasus saya, sedang menulis skrip shell yang dimuliakan tanpa callback, acara, dll.), JANGAN LAKUKAN INI!

Tapi begini caranya Anda bisa melakukan ini:

./calling-file.js

var createClient = require('sync-rpc');
var mySynchronousCall = createClient(require.resolve('./my-asynchronous-call'), 'init data');

var param1 = 'test data'
var data = mySynchronousCall(param1);
console.log(data); // prints: received "test data" after "init data"

./my-asynchronous-call.js

function init(initData) {
  return function(param1) {
    // Return a promise here and the resulting rpc client will be synchronous
    return Promise.resolve('received "' + param1 + '" after "' + initData + '"');
  };
}
module.exports = init;

BATASAN:

Ini adalah konsekuensi dari cara sync-rpcpenerapannya, yaitu dengan menyalahgunakan require('child_process').spawnSync:

  1. Ini tidak akan berfungsi di browser.
  2. Argumen untuk fungsi Anda harus serial. Argumen Anda akan keluar-masuk JSON.stringify, sehingga fungsi dan properti yang tidak dapat dihitung seperti rantai prototipe akan hilang.
Meustrus
sumber
1

Anda juga dapat mengubahnya menjadi panggilan balik.

function thirdPartyFoo(callback) {    
  callback("Hello World");    
}

function foo() {    
  var fooVariable;

  thirdPartyFoo(function(data) {
    fooVariable = data;
  });

  return fooVariable;
}

var temp = foo();  
console.log(temp);
Nikhil
sumber
0

Yang Anda inginkan sebenarnya mungkin sekarang. Jika Anda dapat menjalankan kode asinkron di pekerja layanan, dan kode sinkron di pekerja web, maka Anda dapat meminta pekerja web mengirim XHR sinkron ke pekerja layanan, dan sementara pekerja layanan melakukan hal-hal async, pekerja web utas akan menunggu. Ini bukan pendekatan yang bagus, tetapi bisa berhasil.

Mungkin Anda melihat nama ini.
sumber
-4

Gagasan yang ingin Anda capai dapat dimungkinkan jika Anda mengubah sedikit persyaratan

Kode di bawah ini dimungkinkan jika runtime Anda mendukung spesifikasi ES6.

Lebih lanjut tentang fungsi async

async function myAsynchronousCall(param1) {
    // logic for myAsynchronous call
    return d;
}

function doSomething() {

  var data = await myAsynchronousCall(param1); //'blocks' here until the async call is finished
  return data;
}
eragon512
sumber
4
Firefox memberikan kesalahan: SyntaxError: await is only valid in async functions and async generators. Belum lagi param1 yang tidak didefinisikan (dan bahkan tidak digunakan).
Harvey