Desain async / menunggu JavaScript: kekuatan, jebakan, dan pola penggunaan

Konstruksi async / menunggu muncul dalam standar ES7. Ini dapat dianggap sebagai peningkatan luar biasa dalam bidang pemrograman asinkron dalam JavaScript. Ini memungkinkan Anda untuk menulis kode yang terlihat seperti sinkron, tetapi digunakan untuk menyelesaikan tugas-tugas yang tidak sinkron dan tidak memblokir utas utama. Terlepas dari kenyataan bahwa async / menunggu adalah fitur baru yang hebat dari bahasa, menggunakannya dengan benar tidak begitu sederhana. Bahan, terjemahan yang kami terbitkan hari ini, dikhususkan untuk studi komprehensif async / menunggu dan cerita tentang bagaimana menggunakan mekanisme ini dengan benar dan efektif.

gambar

Kekuatan async / menunggu


Manfaat paling penting yang didapat oleh seorang programmer yang menggunakan konstruk async / await adalah bahwa hal itu memungkinkan untuk menulis kode asinkron dengan gaya yang spesifik untuk kode sinkron. Bandingkan kode yang ditulis menggunakan async / tunggu dengan kode berdasarkan janji.

// async/await async getBooksByAuthorWithAwait(authorId) {  const books = await bookModel.fetchAll();  return books.filter(b => b.authorId === authorId); } //  getBooksByAuthorWithPromise(authorId) {  return bookModel.fetchAll()    .then(books => books.filter(b => b.authorId === authorId)); } 

Mudah untuk memperhatikan bahwa versi async / await dari contoh lebih mudah dipahami daripada versinya, di mana janji digunakan. Jika Anda tidak memperhatikan kata kunci yang await , kode ini akan terlihat seperti serangkaian instruksi reguler yang dijalankan secara serempak - seperti dalam JavaScript yang akrab atau dalam bahasa sinkron lain seperti Python.

Daya tarik async / menunggu tidak hanya karena peningkatan pembacaan kode. Mekanisme ini, di samping itu, menikmati dukungan browser yang sangat baik, yang tidak memerlukan solusi apa pun. Jadi, fungsi asinkron hari ini sepenuhnya mendukung semua browser utama.


Semua browser utama mendukung fungsi asinkron ( caniuse.com )

Tingkat dukungan ini berarti, misalnya, kode yang menggunakan async / menunggu tidak perlu ditransformasikan . Selain itu, ini memfasilitasi debugging, yang mungkin bahkan lebih penting daripada kurangnya kebutuhan untuk transpilasi.

Gambar berikut ini menunjukkan proses debug dari fungsi asinkron. Di sini, ketika mengatur breakpoint pada instruksi pertama fungsi dan ketika menjalankan perintah Step Over, ketika debugger mencapai garis di mana kata kunci await digunakan, Anda dapat melihat bagaimana debugger berhenti sementara waktu, menunggu fungsi bookModel.fetchAll() , dan kemudian melompat ke baris di mana perintah .filter() ! Proses debugging seperti itu terlihat jauh lebih sederhana daripada janji-janji debugging. Di sini, ketika men-debug kode yang sama, Anda harus mengatur breakpoint lain di baris .filter() .


Debugging fungsi asinkron. Debugger akan menunggu baris menunggu untuk menyelesaikan dan pergi ke baris berikutnya setelah operasi selesai

Kekuatan lain dari mekanisme yang dipertimbangkan, yang kurang jelas dari apa yang telah kita bahas, adalah keberadaan kata kunci async sini. Dalam kasus kami, penggunaannya memastikan bahwa nilai yang dikembalikan oleh getBooksByAuthorWithAwait() adalah sebuah janji. Akibatnya, Anda dapat menggunakan getBooksByAuthorWithAwait().then(...) dengan aman getBooksByAuthorWithAwait().then(...) atau await getBooksByAuthorWithAwait() membuat kode yang memanggil fungsi ini. Pertimbangkan contoh berikut (perhatikan bahwa ini tidak disarankan):

 getBooksByAuthorWithPromise(authorId) { if (!authorId) {   return null; } return bookModel.fetchAll()   .then(books => books.filter(b => b.authorId === authorId)); } } 

Di sini fungsi getBooksByAuthorWithPromise() dapat, jika semuanya baik-baik saja, mengembalikan janji, atau, jika terjadi kesalahan - null . Akibatnya, jika terjadi kesalahan, Anda tidak dapat memanggil .then() . Saat mendeklarasikan fungsi menggunakan async kesalahan semacam ini tidak mungkin.

Tentang kesalahan persepsi tentang async / menunggu


Dalam beberapa publikasi, konstruksi async / await dibandingkan dengan janji-janji dan dikatakan mewakili generasi berikutnya dari evolusi pemrograman JavaScript asinkron. Dengan ini, dengan segala hormat kepada penulis publikasi seperti itu, saya membiarkan diri saya tidak setuju. Async / menunggu adalah perbaikan, tetapi tidak lebih dari "gula sintaksis", penampilan yang tidak mengarah pada perubahan lengkap dalam gaya pemrograman.

Intinya, fungsi asinkron adalah janji. Sebelum seorang programmer dapat menggunakan async / wait construct dengan benar, ia harus mempelajari janji dengan baik. Selain itu, dalam kebanyakan kasus, bekerja dengan fungsi asinkron, Anda perlu menggunakan janji.

Lihatlah fungsi getBooksByAuthorWithAwait() dan getBooksByAuthorWithPromises() dari contoh di atas. Harap dicatat bahwa keduanya identik tidak hanya dalam hal fungsionalitas. Mereka juga memiliki antarmuka yang persis sama.

Semua ini berarti bahwa jika Anda secara langsung memanggil fungsi getBooksByAuthorWithAwait() , itu akan mengembalikan janji.

Bahkan, inti dari masalah yang kita bicarakan di sini adalah persepsi yang salah dari desain baru, ketika itu menciptakan perasaan menyesatkan bahwa fungsi sinkron dapat dikonversi menjadi asinkron karena penggunaan sederhana async dan await kata kunci dan tidak memikirkan hal lain.

Jebakan async / menunggu


Mari kita bicara tentang kesalahan paling umum yang bisa dibuat menggunakan async / menunggu. Secara khusus, tentang penggunaan irasional panggilan berturut-turut dari fungsi asinkron.

Meskipun kata kunci yang await dapat membuat kode terlihat seperti sinkron, menggunakannya, perlu diingat bahwa kode tersebut asinkron, yang berarti Anda harus sangat berhati-hati tentang panggilan berurutan dari fungsi-fungsi asinkron.

 async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Kode ini, dalam hal logika, tampaknya benar. Namun, ada masalah serius. Beginilah cara kerjanya.

  1. Panggilan sistem await bookModel.fetchAll() dan tunggu perintah .fetchAll() selesai.
  2. Setelah menerima hasil dari bookModel.fetchAll() await authorModel.fetch(authorId) akan dipanggil.

Perhatikan bahwa panggilan ke authorModel.fetch(authorId) tidak tergantung pada hasil panggilan ke bookModel.fetchAll() , dan, pada kenyataannya, kedua perintah ini dapat dieksekusi secara paralel. Namun, menggunakan await hasil dalam dua panggilan ini dieksekusi secara berurutan. Total waktu eksekusi berurutan dari kedua perintah ini akan lebih lama dari waktu eksekusi paralelnya.

Berikut adalah pendekatan yang benar untuk menulis kode seperti itu:

 async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Perhatikan contoh lain penyalahgunaan fungsi asinkron. Ini masih lebih buruk daripada pada contoh sebelumnya. Seperti yang Anda lihat, untuk memuat daftar elemen tertentu secara tidak sinkron, kita perlu mengandalkan kemungkinan janji.

 async getAuthors(authorIds) { //  ,     // const authors = _.map( //   authorIds, //   id => await authorModel.fetch(id)); //   const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); } 

Singkatnya, maka, untuk menggunakan fungsi asinkron dengan benar, Anda perlu, karena pada saat ini tidak mungkin, pertama-tama pikirkan operasi asinkron, dan kemudian tulis kode menggunakan await . Dalam kasus yang kompleks, mungkin akan lebih mudah menggunakan janji secara langsung.

Menangani kesalahan


Saat menggunakan janji, eksekusi kode asinkron dapat berakhir seperti yang diharapkan - kemudian mereka mengatakan bahwa janji tersebut berhasil diselesaikan, atau dengan kesalahan - kemudian mereka mengatakan bahwa janji tersebut ditolak. Ini memungkinkan kita untuk menggunakan masing-masing. .then() dan .catch() . Namun, penanganan kesalahan menggunakan mekanisme async / await bisa rumit.

▍ coba / tangkap konstruk


Cara standar untuk menangani kesalahan saat menggunakan async / await adalah dengan konstruk try / catch. Saya merekomendasikan menggunakan pendekatan ini. Saat melakukan panggilan tunggu, nilai yang dikembalikan saat janji ditolak disajikan sebagai pengecualian. Berikut ini sebuah contoh:

 class BookModel { fetchAll() {   return new Promise((resolve, reject) => {     window.setTimeout(() => { reject({'error': 400}) }, 1000);   }); } } // async/await async getBooksByAuthorWithAwait(authorId) { try { const books = await bookModel.fetchAll(); } catch (error) { console.log(error);    // { "error": 400 } } 

Kesalahan yang ditangkap di catch adalah persis nilai yang diperoleh saat janji ditolak. Setelah menangkap pengecualian, kami dapat menerapkan beberapa pendekatan untuk bekerja dengannya:

  • Anda dapat menangani pengecualian dan mengembalikan nilai normal. Jika Anda tidak menggunakan ekspresi return di catch untuk mengembalikan apa yang diharapkan setelah fungsi asinkron dijalankan, ini akan setara dengan menggunakan perintah return undefined ;.
  • Anda bisa meneruskan kesalahan ke tempat kode yang gagal dipanggil dan membiarkannya diproses di sana. Anda dapat melempar kesalahan secara langsung dengan menggunakan perintah seperti throw error; , yang memungkinkan Anda untuk menggunakan fungsi async getBooksByAuthorWithAwait() dalam rantai janji. Artinya, dapat dipanggil menggunakan getBooksByAuthorWithAwait().then(...).catch(error => ...) membangun. Selain itu, Anda dapat membungkus kesalahan dalam objek Error , yang mungkin terlihat seperti throw new Error(error) . Ini akan memungkinkan, misalnya, ketika mengeluarkan informasi kesalahan ke konsol, melihat tumpukan panggilan penuh.
  • Kesalahan dapat direpresentasikan sebagai janji yang ditolak, sepertinya return Promise.reject(error) . Dalam hal ini, ini sama dengan perintah throw error , karena itu tidak disarankan.

Berikut adalah manfaat menggunakan konstruk coba / tangkap:

  • Alat penanganan kesalahan seperti itu telah ada dalam pemrograman untuk waktu yang sangat lama, mereka sederhana dan dapat dimengerti. Katakanlah, jika Anda memiliki pengalaman pemrograman dalam bahasa lain, seperti C ++ atau Java, maka Anda akan dengan mudah memahami perangkat try / catch dalam JavaScript.
  • Anda dapat menempatkan beberapa panggilan tunggu dalam satu blok coba / tangkap, yang memungkinkan Anda untuk menangani semua kesalahan di satu tempat jika Anda tidak perlu secara terpisah menangani kesalahan pada setiap langkah dari eksekusi kode.

Perlu dicatat bahwa ada satu kelemahan dalam mekanisme coba / tangkap. Karena try / catch menangkap setiap pengecualian yang terjadi di blok try , pengecualian tersebut yang tidak terkait dengan janji juga akan masuk ke catch handler. Lihatlah contoh ini.

 class BookModel { fetchAll() {   cb();    //    ,   `cb`  ,       return fetch('/books'); } } try { bookModel.fetchAll(); } catch(error) { console.log(error);  //       "cb is not defined" } 

Jika Anda menjalankan kode ini, Anda akan melihat ReferenceError: cb is not defined pesan kesalahan di konsol. Pesan ini dihasilkan oleh perintah console.log() dari catch , dan bukan oleh JavaScript itu sendiri. Dalam beberapa kasus, kesalahan seperti itu menyebabkan konsekuensi yang serius. Misalnya, jika memanggil bookModel.fetchAll(); tersembunyi jauh di dalam serangkaian panggilan fungsi dan salah satu panggilan akan "menelan" kesalahan, itu akan sangat sulit untuk mendeteksi kesalahan seperti itu.

▍ Fungsi mengembalikan dua nilai


Inspirasi untuk cara selanjutnya menangani kesalahan dalam kode asinkron adalah Go. Ini memungkinkan fungsi asinkron untuk mengembalikan kesalahan dan hasilnya. Baca lebih lanjut tentang ini di sini .

Singkatnya, maka fungsi asinkron, dengan pendekatan ini, dapat digunakan seperti ini:

 [err, user] = await to(UserModel.findById(1)); 

Secara pribadi, saya tidak suka ini, karena metode penanganan kesalahan ini memperkenalkan gaya pemrograman Go ke dalam JavaScript, yang terlihat tidak wajar, meskipun, dalam beberapa kasus, ini bisa sangat berguna.

▍Gunakan dari .catch


Cara terakhir untuk menangani kesalahan, yang akan kita bicarakan, adalah dengan menggunakan .catch() .

Pikirkan bagaimana cara await bekerja. Yaitu, penggunaan kata kunci ini menyebabkan sistem menunggu sampai janji itu selesai. Juga, ingatlah bahwa perintah dari form berjanji.catch promise.catch() juga mengembalikan janji. Semua ini menunjukkan bahwa kesalahan fungsi asinkron dapat ditangani seperti ini:

 // books   undefined   , //    catch     let books = await bookModel.fetchAll() .catch((error) => { console.log(error); }); 

Dua masalah kecil adalah karakteristik dari pendekatan ini:

  • Ini adalah campuran dari fungsi janji dan asinkron. Untuk menggunakan ini, perlu, seperti dalam kasus serupa lainnya, untuk memahami fitur-fitur dari pekerjaan yang dijanjikan.
  • Pendekatan ini tidak intuitif, karena penanganan kesalahan dilakukan di tempat yang tidak biasa.

Ringkasan


Konstruksi async / wait, yang diperkenalkan pada ES7, jelas merupakan peningkatan pada mekanisme pemrograman asinkron JavaScript. Ini dapat membuat membaca dan men-debug kode lebih mudah. Namun, untuk menggunakan async / menunggu dengan benar, diperlukan pemahaman yang mendalam tentang janji, karena async / menunggu hanyalah "gula sintaksis" berdasarkan janji.

Kami berharap materi ini memungkinkan Anda untuk menjadi lebih akrab dengan async / menunggu, dan apa yang Anda pelajari di sini akan menyelamatkan Anda dari beberapa kesalahan umum yang muncul saat menggunakan konstruksi ini.

Pembaca yang budiman! Apakah Anda menggunakan konstruk async / menunggu dalam JavaScript? Jika demikian, beri tahu kami bagaimana Anda menangani kesalahan dalam kode asinkron.

Source: https://habr.com/ru/post/id414373/


All Articles