Bagaimana cara menunggu Janji JavaScript diselesaikan sebelum melanjutkan fungsi?

108

Saya melakukan beberapa pengujian unit. Framework pengujian memuat halaman ke dalam iFrame, lalu menjalankan pernyataan pada halaman tersebut. Sebelum setiap tes dimulai, saya membuat Promiseyang menetapkan acara iFrame onloaduntuk dipanggil resolve(), menyetel iFrame src, dan mengembalikan janji.

Jadi, saya bisa menelepon loadUrl(url).then(myFunc), dan itu akan menunggu halaman dimuat sebelum mengeksekusi apa pun myFunc.

Saya menggunakan pola semacam ini di semua tempat dalam pengujian saya (tidak hanya untuk memuat URL), terutama untuk memungkinkan perubahan pada DOM terjadi (misalnya meniru mengklik tombol, dan menunggu div bersembunyi dan ditampilkan).

Kelemahan dari desain ini adalah saya terus-menerus menulis fungsi anonim dengan beberapa baris kode di dalamnya. Lebih lanjut, sementara saya memiliki work-around (QUnit's assert.async()), fungsi pengujian yang mendefinisikan promise selesai sebelum promise dijalankan.

Saya bertanya-tanya apakah ada cara untuk mendapatkan nilai dari a Promiseatau tunggu (blokir / tidur) hingga terselesaikan, mirip dengan .NET IAsyncResult.WaitHandle.WaitOne(). Saya tahu JavaScript adalah single-threaded, tetapi saya berharap itu tidak berarti bahwa suatu fungsi tidak dapat menghasilkan.

Intinya, adakah cara agar yang berikut ini memberikan hasil dalam urutan yang benar?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};

function getResultFrom(promise) {
  // todo
  return " end";
}

var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

dx_over_dt
sumber
jika Anda menempatkan panggilan tambahan ke dalam fungsi yang dapat digunakan kembali, Anda kemudian dapat () sesuai kebutuhan untuk KERING. Anda juga dapat membuat penangan serba guna, yang diarahkan oleh ini untuk memberi makan then () panggilan, seperti .then(fnAppend.bind(myDiv)), yang dapat sangat mengurangi anon.
dandavis
Apa yang Anda uji? Jika ini adalah peramban modern atau Anda dapat menggunakan alat seperti BabelJS untuk mentranspilasi kode Anda, ini pasti mungkin.
Benjamin Gruenbaum

Jawaban:

80

Saya bertanya-tanya apakah ada cara untuk mendapatkan nilai dari Promise atau menunggu (blokir / tidur) hingga terselesaikan, mirip dengan IAsyncResult.WaitHandle.WaitOne () .NET. Saya tahu JavaScript adalah single-threaded, tetapi saya berharap itu tidak berarti bahwa suatu fungsi tidak dapat menghasilkan.

Generasi Javascript saat ini di browser tidak memiliki wait()atau sleep()yang memungkinkan hal lain untuk berjalan. Jadi, Anda tidak bisa melakukan apa yang Anda minta. Sebaliknya, ia memiliki operasi asinkron yang akan melakukan tugasnya dan kemudian memanggil Anda setelah selesai (seperti yang Anda telah gunakan untuk janji).

Sebagian karena threaded tunggal Javascript. Jika utas tunggal berputar, maka Javascript lain tidak dapat mengeksekusi hingga utas pemintalan selesai. ES6 memperkenalkan yielddan generator yang akan memungkinkan beberapa trik kooperatif seperti itu, tetapi kami cukup jauh untuk dapat menggunakannya di banyak browser yang diinstal (mereka dapat digunakan dalam beberapa pengembangan sisi server di mana Anda mengontrol mesin JS yang sedang digunakan).


Pengelolaan kode berbasis janji yang cermat dapat mengontrol urutan eksekusi untuk banyak operasi asinkron.

Saya tidak yakin saya mengerti persis urutan apa yang Anda coba capai dalam kode Anda, tetapi Anda dapat melakukan sesuatu seperti ini menggunakan kickOff()fungsi yang ada , dan kemudian memasang .then()penangan padanya setelah memanggilnya:

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");

    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}

kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

Ini akan mengembalikan keluaran dalam urutan terjamin - seperti ini:

start
middle
end

Pembaruan pada 2018 (tiga tahun setelah jawaban ini ditulis):

Jika Anda mentranspilasi kode Anda atau menjalankan kode Anda di lingkungan yang mendukung fitur ES7 seperti asyncdan await, Anda sekarang dapat menggunakan awaituntuk membuat kode Anda "muncul" untuk menunggu hasil janji. Itu masih berkembang dengan janji. Itu masih tidak memblokir semua Javascript, tetapi memungkinkan Anda untuk menulis operasi berurutan dalam sintaks yang lebih bersahabat.

Alih-alih cara ES6 dalam melakukan sesuatu:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

Kamu bisa melakukan ini:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}

wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});
jfriend00
sumber
3
Benar, menggunakan then()adalah apa yang selama ini saya lakukan. Saya hanya tidak suka menulis function() { ... }sepanjang waktu. Itu mengacaukan kode.
dx_over_dt
3
@dfoverdx - pengkodean asinkron dalam Javascript selalu melibatkan panggilan balik sehingga Anda harus selalu menentukan fungsi (anonim atau bernama). Tidak ada jalan lain saat ini.
jfriend00
2
Padahal Anda bisa menggunakan notasi panah ES6 agar lebih ringkas. (foo) => { ... }bukannyafunction(foo) { ... }
Rob H
1
Menambahkan komentar @RobH cukup sering, Anda juga dapat menulis foo => single.expression(here)untuk menghilangkan tanda kurung keriting dan bulat.
begie
3
@dfoverdx - Tiga tahun setelah jawaban saya, saya memperbaruinya dengan ES7 asyncdan awaitsintaks sebagai opsi yang sekarang tersedia di node.js dan di browser modern atau melalui transpiler.
jfriend00
15

Jika menggunakan ES2016 Anda dapat menggunakan asyncdan awaitdan melakukan sesuatu seperti:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

Jika menggunakan ES2015 Anda dapat menggunakan Generator . Jika Anda tidak menyukai sintaksnya, Anda dapat mengabstraksikannya menggunakan asyncfungsi utilitas seperti yang dijelaskan di sini .

Jika menggunakan ES5 Anda mungkin menginginkan perpustakaan seperti Bluebird untuk memberi Anda kontrol lebih.

Terakhir, jika runtime Anda mendukung ES2015, urutan eksekusi dapat dipertahankan dengan paralelisme menggunakan Fetch Injection .

Josh Habdas
sumber
1
Contoh generator Anda tidak berfungsi, karena kekurangan pelari. Harap jangan merekomendasikan generator lagi ketika Anda hanya dapat memindahkan ES8 async/ await.
Bergi
3
Terima kasih atas tanggapan Anda. Saya telah menghapus contoh Generator dan menautkan ke tutorial Generator yang lebih komprehensif dengan pustaka OSS terkait.
Josh Habdas
11
Ini hanya membungkus janji. Fungsi async tidak akan menunggu apa pun saat Anda menjalankannya, itu hanya akan mengembalikan sebuah promise. async/ awaithanyalah sintaks lain untuk Promise.then.
rustyx
Anda benar asynctidak akan menunggu ... kecuali itu menghasilkan penggunaan await. Contoh ES2016 / ES7 masih tidak dapat dijalankan jadi inilah beberapa kode yang saya tulis tiga tahun lalu yang mendemonstrasikan perilakunya.
Josh Habdas
6

Pilihan lainnya adalah menggunakan Promise.all untuk menunggu serangkaian promise diselesaikan. Kode di bawah ini menunjukkan bagaimana menunggu semua janji diselesaikan dan kemudian menangani hasil setelah semuanya siap (karena tampaknya itulah tujuan pertanyaan); Namun, start jelas bisa menghasilkan keluaran sebelum tengah diselesaikan hanya dengan menambahkan kode itu sebelum panggilan itu menyelesaikan.

function kickOff() {
  let start = new Promise((resolve, reject) => resolve("start"))
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => resolve(" middle"), 1000)
  })
  let end = new Promise((resolve, reject) => resolve(" end"))

  Promise.all([start, middle, end]).then(results => {
    results.forEach(result => $("#output").append(result))
  })
}

kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

Stan Kurdziel
sumber
Ini juga tidak akan berhasil. Ini hanya membuat janji berjalan berurutan. Apa pun setelah kickOff () akan berjalan sebelum promise selesai.
Philip Rego
1

Anda bisa melakukannya secara manual. (Saya tahu, itu bukan solusi yang bagus, tapi ..) gunakan whileloop sampai resulttidak memiliki nilai

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});
Guga Nemsitsveridze
sumber
1
ini akan menghabiskan seluruh cpu sambil menunggu.
Lukasz Guminski
ini adalah solusi yang mengerikan
JSON
Ya tapi ini satu-satunya cara yang akan berhasil tanpa harus menyarangkan fungsi atau janji. Meski begitu, baris setelah fungsi / janji bertingkat dijalankan sebelum selesai.
Philip Rego
Ini tidak akan berhasil. Karena Anda berada dalam loop tak terbatas menunggu untuk resultberubah, hasil tidak akan pernah memiliki kesempatan untuk berubah karena event loop tidak pernah bisa memproses yang lain. Dan, resultini hanyalah argumen fungsi yang tidak pernah berubah.
jfriend00