Bahkan anak-anak akan mengerti: penjelasan sederhana tentang async / menunggu dan janji dalam JavaScript

Halo, Habr! Saya mempersembahkan untuk Anda terjemahan artikel “JavaScript Async / Menunggu dan Menjanjikan: Dijelaskan seperti Anda berusia lima tahun” oleh Jack Pordi.

Siapa pun yang menganggap dirinya sebagai pengembang JavaScript, pada titik tertentu, harus mengalami fungsi panggilan balik, janji, atau, baru-baru ini, sintaks async / menunggu. Jika Anda sudah cukup lama bermain, Anda mungkin akan menemukan saat-saat ketika fungsi panggilan balik bersarang adalah satu-satunya cara untuk mencapai sinkronisasi di JavaScript.

Ketika saya mulai belajar dan menulis dalam JavaScript, sudah ada satu miliar tutorial dan tutorial yang menjelaskan cara mencapai asinkron dalam JavaScript. Namun, banyak dari mereka hanya menjelaskan cara mengubah fungsi panggilan balik ke janji atau janji di async / menunggu. Bagi banyak orang, ini mungkin lebih dari cukup untuk bergaul dengan mereka dan mulai menggunakannya dalam kode mereka.

Namun, jika Anda, seperti saya, benar-benar ingin memahami pemrograman asinkron (dan bukan hanya sintaksis JavaScript!), Maka Anda mungkin setuju dengan saya bahwa ada kekurangan materi yang menjelaskan pemrograman asinkron dari awal.

Apakah yang dimaksud dengan asinkron?


gambar menunjukkan orang yang berpikir

Sebagai aturan, menanyakan pertanyaan ini, Anda dapat mendengar sesuatu dari yang berikut:

  • Ada beberapa utas yang mengeksekusi kode pada saat bersamaan.
  • Lebih dari satu bagian kode dieksekusi sekaligus.
  • Ini konkurensi.

Sampai batas tertentu, semua opsi sudah benar. Tetapi alih-alih memberi Anda definisi teknis yang kemungkinan akan segera Anda lupakan, saya akan memberikan contoh yang bahkan seorang anak pun bisa mengerti .

Contoh hidup


gambar menunjukkan sayuran dan pisau dapur

Bayangkan Anda sedang memasak sup sayur. Untuk analogi yang bagus dan sederhana, misalkan sup sayuran hanya terdiri dari bawang dan wortel. Resep untuk sup seperti itu adalah sebagai berikut:

  1. Potong wortel.
  2. Potong bawang.
  3. Tambahkan air ke wajan, nyalakan kompor dan tunggu sampai mendidih.
  4. Tambahkan wortel ke dalam wajan dan biarkan selama 5 menit.
  5. Tambahkan bawang ke wajan dan masak selama 10 menit.

Petunjuk ini sederhana dan dapat dimengerti, tetapi jika salah satu dari Anda, membaca ini, benar-benar tahu cara memasak, Anda dapat mengatakan bahwa ini bukan cara memasak yang paling efektif. Dan Anda akan benar, itu sebabnya:

  • Langkah 3, 4 dan 5 sebenarnya tidak mengharuskan Anda sebagai koki untuk melakukan apa pun kecuali untuk mengamati proses dan melacak waktu.
  • Langkah 1 dan 2 mengharuskan Anda untuk secara aktif melakukan sesuatu.

Oleh karena itu, resep untuk koki yang lebih berpengalaman mungkin sebagai berikut:

  1. Mulailah merebus panci air.
  2. Sambil menunggu panci mendidih, mulailah memotong wortel.
  3. Saat Anda selesai memotong wortel, air akan mendidih, jadi tambahkan wortel.
  4. Sementara wortel dimasak dalam wajan, potong bawang.
  5. Tambahkan bawang dan masak 10 menit lagi.

Terlepas dari kenyataan bahwa semua tindakan tetap sama, Anda memiliki hak untuk berharap bahwa opsi ini akan jauh lebih cepat dan lebih efisien. Ini persis prinsip pemrograman asinkron: Anda tidak akan pernah ingin duduk, hanya menunggu sesuatu, sementara Anda bisa menghabiskan waktu Anda pada beberapa hal berguna lainnya.

Kita semua tahu bahwa dalam pemrograman, menunggu sesuatu terjadi cukup sering - apakah menunggu respons HTTP dari server atau tindakan dari pengguna atau sesuatu yang lain. Tetapi siklus eksekusi prosesor Anda sangat berharga dan harus selalu digunakan secara aktif, melakukan sesuatu, dan tidak mengharapkan: ini menghasilkan pemrograman asinkron .

Sekarang mari kita ke JavaScript, oke?


Jadi, mengikuti contoh yang sama dari sup sayuran, saya akan menulis beberapa fungsi untuk mewakili langkah-langkah resep yang dijelaskan di atas.

Pertama, mari kita menulis fungsi sinkron yang mewakili tugas yang tidak memakan waktu. Ini adalah fungsi-fungsi JavaScript lama yang baik, tetapi perhatikan bahwa saya telah mendeskripsikan chopOnions dan chopOnions sebagai tugas yang membutuhkan kerja aktif (dan waktu), yang memungkinkan mereka melakukan beberapa perhitungan panjang. Kode lengkap tersedia di akhir artikel [1].

 function chopCarrots() { /*   ... */ console.log(" !"); } function chopOnions() { /*   ... */ console.log(" !"); } function addOnions() { console.log("   !"); } function addCarrots() { console.log("   !"); } 

Sebelum beralih ke fungsi asinkron, saya pertama-tama akan menjelaskan bagaimana sistem tipe JavaScript menangani asinkron: pada dasarnya semua hasil (termasuk kesalahan) operasi asinkron harus dibungkus dengan janji .

Agar suatu fungsi mengembalikan janji, Anda dapat:

  • secara eksplisit mengembalikan janji, yaitu return new Promise(…) ;
  • secara implisit mengembalikan janji - tambahkan async ke deklarasi fungsi, mis. async function foo() ;
  • gunakan kedua opsi .

Ada artikel yang bagus [2], yang membahas tentang perbedaan antara fungsi dan fungsi asinkron yang menghasilkan janji. Oleh karena itu, dalam artikel saya, saya tidak akan membahas topik ini, hal utama yang perlu diingat: Anda harus selalu menggunakan async dalam fungsi asinkron.

Jadi, fungsi asinkron kami, mewakili langkah 3-5 dalam menyiapkan sup sayur, adalah sebagai berikut:

 async function letPotKeepBoiling(time) { return; //  ,      } async function boilPot() { return; //  ,       } 

Sekali lagi, saya menghapus detail implementasi agar tidak terganggu oleh mereka, tetapi mereka diterbitkan pada akhir artikel [1].

Penting untuk diketahui bahwa untuk menunggu hasil dari janji, sehingga nantinya Anda dapat melakukan sesuatu dengannya, Anda cukup menggunakan kata kunci await :

 async function asyncFunction() { /*  ... */ } result = await asyncFunction(); 

Jadi, sekarang kita hanya perlu menyatukannya:

 function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("   !"); } makeSoup(); 

Tapi tunggu! Ini tidak bekerja! Anda akan melihat SyntaxError: await is only valid in async functions kesalahan SyntaxError: await is only valid in async functions . Mengapa Karena jika Anda tidak mendeklarasikan fungsi menggunakan async , maka secara default JavaScript mendefinisikannya sebagai fungsi sinkron - dan sinkron berarti tidak menunggu! [3]. Ini juga berarti bahwa Anda tidak dapat menggunakan await luar fungsi.

Karenanya, kami cukup menambahkan async ke fungsi makeSoup :

 async function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("   !"); } makeSoup(); 

Dan voila! Perhatikan bahwa pada baris kedua, saya memanggil fungsi asinkron boilPot tanpa await kata kunci, karena kami tidak ingin menunggu wajan mendidih sebelum kami mulai memotong wortel. Kita hanya mengharapkan janji pot di baris kelima sebelum kita perlu meletakkan wortel di dalam wajan, karena kita tidak ingin melakukan ini sebelum air mendidih.

Apa yang terjadi selama await panggilan? Yah, tidak ada ... semacam ...

Dalam konteks fungsi makeSoup Anda bisa menganggapnya sebagai sesuatu yang Anda harapkan terjadi (atau hasil yang pada akhirnya akan dikembalikan).

Tapi ingat: Anda (seperti prosesor Anda) tidak akan pernah mau hanya duduk di sana dan menunggu sesuatu, sementara Anda dapat menghabiskan waktu Anda untuk hal-hal lain .

Karena itu, alih-alih hanya memasak sup, kita bisa memasak sesuatu yang lain secara paralel:

 makeSoup(); makePasta(); 

Sementara kita menunggu letPotKeepBoiling , kita dapat, misalnya, memasak pasta.

Lihat? Sintaks async / await sebenarnya cukup mudah digunakan, jika Anda memahaminya, setuju?

Bagaimana dengan janji terbuka?


Nah, jika Anda bersikeras, saya akan beralih ke penggunaan janji-janji eksplisit ( komentar terjemahan : dengan janji-janji eksplisit, penulis menyiratkan secara langsung sintaks dari janji-janji, dan secara implisit menjanjikan sintaksis async / menunggu, karena ia mengembalikan janji secara implisit - tidak perlu menulis return new Promise(…) ). Perlu diingat bahwa metode async / menunggu didasarkan pada janji itu sendiri, dan oleh karena itu kedua opsi sepenuhnya kompatibel .

Janji-janji eksplisit, menurut pendapat saya, ada di antara panggilan balik gaya lama dan sintaksis seksual baru yang menunggu. Atau, Anda juga dapat menganggap sintaksis seksual async / menunggu sebagai tidak lebih dari janji tersirat. Pada akhirnya, async / menunggu menunggu datang setelah janji, yang pada gilirannya datang setelah fungsi panggilan balik.

Gunakan mesin waktu kami untuk pindah ke neraka panggilan balik [4]:

 function callbackHell() { boilPot( () => { addCarrots(); letPotKeepBoiling(() => { addOnions(); letPotKeepBoiling(() => { console.log("   !"); }, 1000); }, 5000); }, 5000, chopCarrots(), chopOnions() ); } 

Saya tidak akan berbohong, saya menulis contoh ini dengan cepat ketika saya mengerjakan artikel ini, dan butuh waktu lebih lama daripada yang ingin saya akui. Banyak dari Anda mungkin tidak tahu apa yang sedang terjadi di sini. Sahabatku, bukankah semua fungsi panggilan balik ini mengerikan? Jadikan itu pelajaran untuk tidak pernah menggunakan fungsi callback lagi ...

Dan, seperti yang dijanjikan, contoh yang sama dengan janji eksplisit:

 function makeSoup() { return Promise.all([ new Promise((reject, resolve) => { chopCarrots(); chopOnions(); resolve(); }), boilPot() ]) .then(() => { addCarrots(); return letPotKeepBoiling(5); }) .then(() => { addOnions(); return letPotKeepBoiling(10); }) .then(() => { console.log("   !"); }); } 

Seperti yang Anda lihat, janji masih mirip dengan fungsi panggilan balik.
Saya tidak akan merinci, tetapi yang paling penting:

  • .then adalah metode janji yang mengambil hasilnya dan meneruskannya ke fungsi argumen (pada dasarnya, ke fungsi panggilan balik ...)
  • Anda tidak pernah dapat menggunakan hasil dari janji di luar konteks .then . Intinya, .kemudian seperti blok asinkron yang mengharapkan hasil dan kemudian meneruskannya ke fungsi panggilan balik.
  • Selain metode .then , ada metode lain dalam .catch - .catch Diperlukan untuk menangani kesalahan dalam janji-janji. Tetapi saya tidak akan membahas lebih rinci, karena sudah ada satu miliar artikel dan tutorial tentang topik ini.

Kesimpulan


Saya harap Anda mendapat beberapa gagasan tentang janji dan pemrograman asinkron dari artikel ini, atau mungkin setidaknya belajar contoh yang baik dari kehidupan untuk menjelaskan hal ini kepada orang lain.

Jadi, ke arah mana Anda menggunakan: janji atau async / menunggu?
Jawabannya sepenuhnya terserah Anda - dan saya akan mengatakan bahwa menggabungkan mereka tidak terlalu buruk, karena kedua pendekatan tersebut sepenuhnya kompatibel satu sama lain.

Namun demikian, saya pribadi 100% di async / menunggu camp, karena bagi saya kode jauh lebih dimengerti dan lebih baik mencerminkan multitasking sebenarnya dari pemrograman asynchronous.



[1] : Kode sumber lengkap tersedia di sini .
[2] : Artikel asli “Fungsi Async vs. fungsi yang mengembalikan Janji " , terjemahan artikel " Perbedaan antara fungsi asinkron dan fungsi yang mengembalikan janji . "
[3] : Anda dapat berargumen bahwa JavaScript mungkin dapat menentukan tipe async / tunggu berdasarkan fungsi dan memeriksa secara rekursif, tetapi JavaScript tidak dirancang untuk menjaga keamanan tipe statis pada waktu kompilasi, belum lagi yang jauh lebih nyaman bagi pengembang untuk secara eksplisit melihat jenis fungsi.
[4] : Saya menulis fungsi “asinkron”, dengan asumsi mereka berfungsi di bawah antarmuka yang sama dengan setTimeout . Perhatikan bahwa panggilan balik tidak kompatibel dengan janji dan sebaliknya.

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


All Articles