Penjelasan pembicaraan tentang pemrograman asinkron dalam Javascript

Halo semuanya!

Seperti yang Anda ingat, kembali pada bulan Oktober kami menerjemahkan artikel yang menarik tentang penggunaan penghitung waktu dalam Javascript. Itu menyebabkan diskusi besar, sesuai dengan hasil yang telah lama kami inginkan untuk kembali ke topik ini dan menawarkan kepada Anda analisis terperinci tentang pemrograman asinkron dalam bahasa ini. Kami senang bahwa kami berhasil menemukan materi yang layak dan menerbitkannya sebelum akhir tahun. Selamat membaca!

Pemrograman asinkron dalam Javascript telah melalui evolusi multi-tahap: dari panggilan balik ke janji dan selanjutnya ke generator, dan segera ke async/await . Pada setiap tahap, pemrograman asinkron dalam Javascript sedikit disederhanakan bagi mereka yang sudah berlutut dalam bahasa ini, tetapi bagi para pemula itu menjadi lebih menakutkan, karena itu perlu untuk memahami nuansa masing-masing paradigma, menguasai aplikasi masing-masing dan, yang tidak kalah penting, untuk memahami, bagaimana cara kerjanya.

Dalam artikel ini, kami memutuskan untuk mengingat secara singkat cara menggunakan panggilan balik dan janji, memberikan pengantar singkat untuk generator, dan kemudian membantu Anda untuk memahami secara tepat bagaimana pemrograman asinkron β€œdi bawah tenda” dengan generator dan async / menunggu diatur. Kami berharap bahwa dengan cara ini Anda dapat dengan percaya diri menerapkan berbagai paradigma tepat di mana mereka sesuai.

Diasumsikan bahwa pembaca telah menggunakan panggilan balik, janji, dan generator untuk pemrograman asinkron, dan juga cukup akrab dengan penutupan dan penguraian dalam Javascript.

Panggilan balik neraka

Awalnya, ada panggilan balik. Javascript tidak memiliki I / O sinkron (selanjutnya disebut I / O) dan pemblokiran tidak didukung sama sekali. Jadi, untuk mengatur I / O atau untuk menunda tindakan apa pun, strategi seperti itu dipilih: kode yang diperlukan untuk dieksekusi secara serempak diteruskan ke fungsi dengan eksekusi yang ditunda, yang diluncurkan di suatu tempat di bawah dalam loop acara. Satu panggilan balik tidak terlalu buruk, tetapi kode tumbuh, dan panggilan balik biasanya menelurkan panggilan balik baru. Hasilnya kira-kira seperti ini:

 getUserData(function doStuff(e, a) { getMoreUserData(function doMoreStuff(e, b) { getEvenMoreUserData(function doEvenMoreStuff(e, c) { getYetMoreUserData(function doYetMoreStuff(e, c) { console.log('Welcome to callback hell!'); }); }); }); }) 

Selain dari benjolan angsa ketika melihat kode fraktal, ada satu masalah lagi: sekarang kami telah mendelegasikan kontrol do*Stuff logika do*Stuff ke fungsi lain ( get*UserData() ), di mana Anda mungkin tidak memiliki kode sumber, dan Anda mungkin tidak yakin apakah mereka melakukan panggilan balik Anda. Bagus bukan?

Janji

Janji membalikkan inversi kontrol yang disediakan oleh callback dan membantu mengurai kusut callback dalam rantai yang mulus.
Sekarang contoh sebelumnya dapat dikonversi menjadi sesuatu seperti ini:

 getUserData() .then(getUserData) .then(doMoreStuff) .then(getEvenMoreUserData) .then(doEvenMoreStuff) .then(getYetMoreUserData) .then(doYetMoreStuff); 

Sudah tidak begitu jelek, ya?

Tapi, biarkan aku !!! Mari kita lihat contoh callback yang lebih vital (tapi masih dibuat-buat):

 // ,     fetchJson(),   GET   , //    :         ,     –   // . function fetchJson(url, callback) { ... } fetchJson('/api/user/self', function(e, user) { fetchJson('/api/interests?userId=' + user.id, function(e, interests) { var recommendations = []; interests.forEach(function () { fetchJson('/api/recommendations?topic=' + interest, function(e, recommendation) { recommendations.push(recommendation); if (recommendations.length == interests.length) { render(profile, interests, recommendations); } }); }); }); }); 

Jadi, kami memilih profil pengguna, lalu minatnya, lalu, berdasarkan minatnya, kami memilih rekomendasi dan, akhirnya, setelah mengumpulkan semua rekomendasi, kami menampilkan halaman. Serangkaian panggilan balik seperti itu, yang, mungkin, bisa dibanggakan, tetapi, bagaimanapun, itu entah bagaimana berbulu. Tidak ada, terapkan janji di sini - dan semuanya akan berhasil. Benar?

Mari kita ubah metode fetchJson() sehingga ia mengembalikan janji alih-alih menerima panggilan balik. Sebuah janji diselesaikan oleh badan tanggapan yang diuraikan dalam format JSON.

 fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (interests) { return Promise.all[interests.map(i => fetchJson('/api/recommendations?topic=' + i))]; }) .then(function (recommendations) { render(user, interests, recommendations); }); 

Bagus kan? Apa yang salah dengan kode ini sekarang?

... Ups! ..
Kami tidak memiliki akses ke profil atau minat pada fungsi terakhir rantai ini? Jadi tidak ada yang berhasil! Apa yang harus dilakukan Mari kita coba janji-janji yang tersarang:

 fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id) .then(interests => { user: user, interests: interests }); }) .then(function (blob) { return Promise.all[blob.interests.map(i => fetchJson('/api/recommendations?topic=' + i))] .then(recommendations => { user: blob.user, interests: blob.interests, recommendations: recommendations }); }) .then(function (bigBlob) { render(bigBlob.user, bigBlob.interests, bigBlob.recommendations); }); 

Ya ... sekarang terlihat jauh lebih canggung dari yang kami harapkan. Apakah itu karena boneka bersarang yang gila sehingga kami, yang tak kalah pentingnya, berusaha keluar dari neraka panggilan balik? Apa yang harus dilakukan sekarang?

Kode dapat disisir sedikit, bersandar pada penutupan:

 //   ,     var user, interests; fetchJson('/api/user/self') .then(function (fetchedUser) { user = fetchedUser; return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (fetchedInterests) { interests = fetchedInterests; return Promise.all(interests.map(i => fetchJson('/api/recommendations?topic=' + i))); }) .then(function (recomendations) { render(user, interests, recommendations); }) .then(function () { console.log('We are done!'); }); 

Ya, sekarang semuanya praktis seperti yang kita inginkan, tetapi dengan satu kekhasan. Perhatikan bagaimana kami menyebut argumen di dalam callback di fetchedUser yang fetchedUser dan fetchedInterests , bukan fetchedInterests user dan interests ? Jika demikian, maka Anda sangat jeli!

Kelemahan dari pendekatan ini adalah ini: Anda harus sangat, sangat berhati-hati untuk tidak menyebutkan apa pun dalam fungsi internal serta variabel dari cache yang akan Anda gunakan dalam penutupan Anda. Bahkan jika Anda memiliki bakat untuk menghindari membayangi, merujuk variabel yang begitu tinggi dalam penutupan masih tampak cukup berbahaya, dan itu jelas tidak baik.

Generator Asinkron

Generator akan membantu! Jika Anda menggunakan generator, maka semua kegembiraan hilang. Hanya sulap. Yang benar adalah. Coba lihat saja:

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

Itu saja. Itu akan berhasil. Anda tidak menangis ketika Anda melihat betapa indahnya generator itu, apakah Anda menyesal bahwa Anda begitu picik dan mulai belajar Javascript bahkan sebelum generator muncul di dalamnya? Saya akui, ide seperti itu pernah mengunjungi saya.
Tapi ... bagaimana cara kerjanya? Sungguh ajaib?

Tentu saja! .. Tidak. Kami beralih ke paparan.

Generator

Dalam contoh kita, tampaknya generator itu mudah digunakan, tetapi sebenarnya ada banyak hal yang terjadi di dalamnya. Untuk memahami generator asinkron secara lebih rinci, Anda perlu lebih memahami bagaimana generator bekerja dan bagaimana mereka memberikan eksekusi asinkron, yang tampaknya sinkron.

Seperti namanya, generator membuat nilai:

 function* counts(start) { yield start + 1; yield start + 2; yield start + 3; return start + 4; } const counter = counts(0); console.log(counter.next()); // {value: 1, done: false} console.log(counter.next()); // {value: 2, done: false} console.log(counter.next()); // {value: 3, done: false} console.log(counter.next()); // {value: 4, done: true} console.log(counter.next()); // {value: undefined, done: true} 

Ini cukup sederhana, tapi bagaimanapun, mari kita bicara tentang apa yang terjadi di sini:

  1. const counter = counts(); - inisialisasi generator dan simpan di penghitung variabel. Generator ada dalam limbo, belum ada kode di tubuh generator yang dieksekusi.
  2. console.log(counter.next()); - Interpretasi output ( yield ) 1, setelah itu 1 dikembalikan sebagai value , dan done menghasilkan false , karena output tidak berakhir di sana
  3. console.log(counter.next()); - Sekarang 2!
  4. console.log(counter.next()); - Sekarang 3! Selesai Apakah semuanya benar? Tidak. Eksekusi dijeda dalam langkah yield 3; Untuk menyelesaikan, Anda perlu menelepon next () lagi.
  5. console.log(counter.next()); - Sekarang 4, dan ia kembali, tetapi tidak dikeluarkan, jadi sekarang kita keluar dari fungsinya, dan semuanya siap.
  6. console.log(counter.next()); - Generator sudah selesai bekerja! Dia tidak perlu melaporkan apa pun kecuali "segala sesuatu dilakukan."

Jadi kami menemukan cara kerja generator! Tapi tunggu, kebenaran yang mengejutkan: generator tidak hanya bisa memuntahkan nilai, tetapi juga melahapnya!

 function* printer() { console.log("We are starting!"); console.log(yield); console.log(yield); console.log(yield); console.log("We are done!"); } const counter = printer(); counter.next(1); // ! counter.next(2); // 2 counter.next(3); // 3 counter.next(4); // 4\n ! counter.next(5); //    

Fiuh, apa ?! Generator mengkonsumsi nilai, alih-alih menelurkannya. Bagaimana ini mungkin?

Rahasianya ada di fungsi next . Ini tidak hanya mengembalikan nilai dari generator, tetapi juga dapat mengembalikannya ke generator. Jika Anda memberi tahu argumen next() , maka operasi yield , yang sedang menunggu generator, sebenarnya menghasilkan argumen. Inilah sebabnya mengapa counter.next(1) pertama counter.next(1) terdaftar sebagai undefined . Tidak ada ekstradisi yang bisa diselesaikan.

Seolah-olah generator memungkinkan kode panggilan (prosedur) dan kode generator (prosedur) untuk bermitra satu sama lain sehingga mereka akan memberikan nilai satu sama lain ketika mereka dieksekusi dan menunggu satu sama lain. Situasinya praktis sama, seolah-olah untuk generator Javascript kemungkinan menerapkan prosedur yang dijalankan secara kompetitif, mereka juga "coroutine", akan dipikirkan. Sebenarnya, hampir seperti co() , kan?

Tapi jangan terburu-buru, kalau tidak kita akan mengecoh diri kita sendiri. Dalam hal ini, penting bahwa pembaca secara intuitif memahami esensi generator dan pemrograman asinkron, dan cara terbaik untuk melakukan ini adalah dengan merakit generator sendiri. Jangan menulis fungsi generator dan jangan menggunakan yang sudah jadi, tetapi buat ulang interior fungsi generator sendiri.

Perangkat internal generator - kami menghasilkan generator

Oke, saya benar-benar tidak tahu bagaimana persisnya generator internal terlihat di runtime JS yang berbeda. Tetapi ini tidak begitu penting. Generator sesuai dengan antarmuka. Sebuah "konstruktor" untuk instantiating generator, metode next(value? : any) , dengan mana kita memerintahkan generator untuk terus bekerja dan memberikan nilai-nilai itu, metode throw(error) dalam kasus throw(error) dihasilkan bukan nilai, dan akhirnya, metode return() , yang masih diam. Jika kepatuhan dengan antarmuka tercapai, maka semuanya baik-baik saja.

Jadi, mari kita coba untuk membangun counts() disebutkan di atas pada ES5 murni, tanpa function* kata kunci function* . Untuk saat ini, Anda dapat mengabaikan throw() dan meneruskan nilainya ke next() , karena metode ini tidak menerima input apa pun. Bagaimana cara melakukannya?

Namun dalam Javascript, ada mekanisme lain untuk menjeda dan melanjutkan eksekusi program: closures! Apakah itu terlihat familier?

 function makeCounter() { var count = 1; return function () { return count++; } } var counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 

Jika Anda menggunakan penutupan sebelumnya, saya yakin Anda sudah menulis sesuatu seperti itu. Fungsi yang dikembalikan oleh makeCounter dapat menghasilkan urutan angka yang tak terbatas, seperti halnya generator.

Namun, fungsi ini tidak sesuai dengan antarmuka generator, dan tidak dapat langsung diterapkan dalam contoh kami dengan counts() , yang mengembalikan 4 nilai dan keluar. Apa yang dibutuhkan untuk pendekatan universal untuk menulis fungsi seperti generator?

Penutupan, mesin negara, dan kerja keras!

 function counts(start) { let state = 0; let done = false; function go() { let result; switch (state) { case 0: result = start + 1; state = 1; break; case 1: result = start + 2; state = 2; break; case 2: result = start + 3; state = 3; break; case 3: result = start + 4; done = true; state = -1; break; default: break; } return {done: done, value: result}; } return { next: go } } const counter = counts(0); console.log(counter.next()); // {value: 1, done: false} console.log(counter.next()); // {value: 2, done: false} console.log(counter.next()); // {value: 3, done: false} console.log(counter.next()); // {value: 4, done: true} console.log(counter.next()); // {value: undefined, done: true} 

Dengan menjalankan kode ini, Anda akan melihat hasil yang sama seperti dalam versi dengan generator. Bagus kan?
Jadi, kami memilah-milah sisi generator; mari kita menganalisis konsumsi?
Padahal, tidak banyak perbedaan.

 function printer(start) { let state = 0; let done = false; function go(input) { let result; switch (state) { case 0: console.log("We are starting!"); state = 1; break; case 1: console.log(input); state = 2; break; case 2: console.log(input); state = 3; break; case 3: console.log(input); console.log("We are done!"); done = true; state = -1; break; default: break; return {done: done, value: result}; } } return { next: go } } const counter = printer(); counter.next(1); // ! counter.next(2); // 2 counter.next(3); // 3 counter.next(4); // 4 counter.next(5); // ! 

Semua yang diperlukan adalah menambahkan input sebagai argumen go , dan nilai-nilai disalurkan. Tampak seperti sihir lagi? Hampir seperti generator?

Hore! Jadi kami menciptakan kembali generator sebagai pemasok dan konsumen. Mengapa tidak mencoba menggabungkan fungsi-fungsi ini di dalamnya? Ini adalah contoh lain dari generator:

 function* adder(initialValue) { let sum = initialValue; while (true) { sum += yield sum; } } 

Karena kita semua adalah spesialis dalam generator, kita memahami bahwa generator ini menambahkan nilai yang diberikan dalam jumlah next(value) , dan kemudian mengembalikan jumlah. Ini bekerja persis seperti yang kami harapkan:

 const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.next(3)); // 6 

Keren Sekarang mari kita tulis antarmuka ini sebagai fungsi normal!

 function adder(initialValue) { let state = 'initial'; let done = false; let sum = initialValue; function go(input) { let result; switch (state) { case 'initial': result = initialValue; state = 'loop'; break; case 'loop': sum += input; result = sum; state = 'loop'; break; default: break; } return {done: done, value: result}; } return { next: go } } function runner() { const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.next(3)); // 6 } runner(); 

Wow, kami telah menerapkan coroutine lengkap.

Masih ada sesuatu untuk dibahas tentang operasi generator. Bagaimana cara kerja pengecualian? Dengan pengecualian yang terjadi di dalam generator, semuanya sederhana: next() akan membuat pengecualian mencapai pemanggil dan generator akan mati. Melewati pengecualian ke generator dilakukan dalam metode throw() , yang kami hapus di atas.

Mari memperkaya terminator kami dengan fitur baru yang keren. Jika pemanggil melewati pengecualian ke generator, itu akan kembali ke nilai terakhir dari jumlah.

 function* adder(initialValue) { let sum = initialValue; let lastSum = initialValue; let temp; while (true) { try { temp = sum; sum += yield sum; lastSum = temp; } catch (e) { sum = lastSum; } } } const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.throw(new Error('BOO)!'))); // 1 console.log(add.next(4)); // 5 

Masalah Pemrograman - Penetrasi Kesalahan Generator

Kamerad, bagaimana kita menerapkan throw ()?

Mudah! Kesalahan hanyalah nilai lain. Kita dapat meneruskannya go() sebagai argumen berikutnya. Bahkan, beberapa kehati-hatian dibutuhkan di sini. Ketika throw(e) dipanggil, yield akan bekerja seperti jika kita telah menulis lemparan e. Ini berarti bahwa kita harus memeriksa kesalahan setiap keadaan mesin negara kita, dan crash program jika kita tidak dapat menangani kesalahan.

Mari kita mulai dengan implementasi terminator sebelumnya, disalin

Pola

Solusi

Boom! Kami telah mengimplementasikan seperangkat coroutine yang mampu menyampaikan pesan dan pengecualian satu sama lain, seperti generator yang sebenarnya.

Tapi situasinya semakin buruk, bukan? Implementasi mesin negara semakin menjauh dari implementasi generator. Tidak hanya itu, karena penanganan kesalahan, kode ini berantakan dengan sampah; kode ini semakin rumit karena while yang kita miliki di sini. Untuk mengonversi while Anda perlu "melepaskannya" menjadi status. Jadi, kasus kami 1 sebenarnya termasuk 2,5 iterasi while , karena yield istirahat di tengah. Akhirnya, Anda harus menambahkan kode tambahan untuk mendorong pengecualian dari pemanggil dan sebaliknya jika tidak ada try/catch di generator untuk menangani pengecualian ini.

Anda berhasil !!! Kami telah menyelesaikan analisis terperinci tentang kemungkinan alternatif untuk penerapan generator dan, saya harap, Anda sudah lebih memahami cara kerja generator. Dalam residu kering:

  • Generator dapat menghasilkan nilai, mengkonsumsi nilai, atau keduanya.
  • Keadaan generator dapat dijeda (keadaan, kondisi mesin, tangkap?)
  • Penelepon dan generator memungkinkan Anda untuk membentuk satu set corutin, berinteraksi satu sama lain
  • Pengecualian diteruskan ke segala arah.

Sekarang kita lebih berpengalaman dalam generator, saya mengusulkan cara yang berpotensi nyaman untuk alasan tentang mereka: ini adalah konstruksi sintaksis yang dengannya Anda dapat menulis prosedur yang dijalankan secara kompetitif yang saling menyampaikan nilai satu sama lain melalui saluran yang mengirimkan nilai satu per satu ( yield ). Ini akan berguna di bagian selanjutnya, di mana kami akan menghasilkan implementasi co() dari coroutine.

Inversi kontrol korutin

Sekarang kita sudah terampil dalam bekerja dengan generator, mari kita pikirkan bagaimana mereka dapat digunakan dalam pemrograman asinkron. Jika kita dapat menulis generator seperti itu, ini tidak berarti bahwa janji di generator akan secara otomatis diselesaikan. Tapi tunggu, generator tidak dimaksudkan untuk bekerja sendiri. Mereka harus berinteraksi dengan program lain, prosedur utama, yang memanggil .next() dan .throw() .

Bagaimana jika kita menempatkan logika bisnis kita bukan di prosedur utama, tetapi di generator? Setiap kali nilai asinkron tertentu terjadi, seperti janji, untuk logika bisnis, generator akan berkata: "Saya tidak ingin mengacaukan omong kosong ini, bangunkan saya ketika sudah beres", akan berhenti sebentar dan mengeluarkan janji untuk prosedur penyajian. Prosedur perawatan: "OK, saya akan menghubungi Anda nanti." Setelah itu mendaftarkan panggilan balik dengan janji ini, keluar dan menunggu sampai dimungkinkan untuk memicu siklus peristiwa (yaitu, ketika janji terselesaikan). Ketika ini terjadi, prosedur akan mengumumkan "hei, giliran Anda," dan mengirim nilainya melalui .next() generator tidur. Dia akan menunggu generator untuk melakukan tugasnya, dan sementara itu ia akan melakukan hal-hal asinkron lainnya ... dan seterusnya. Anda mendengarkan kisah sedih tentang bagaimana prosedur ini berlangsung untuk generator.

Jadi, kembali ke topik utama. Sekarang kita tahu bagaimana generator dan janji bekerja, tidak akan sulit bagi kita untuk membuat "prosedur layanan" seperti itu. Prosedur layanan itu sendiri akan dilaksanakan secara kompetitif sebagai janji, instantiate dan memelihara generator, dan kemudian kembali ke hasil akhir dari prosedur utama kami menggunakan callback .then() .

Selanjutnya, mari kita kembali ke program co () dan membahasnya secara lebih rinci. co() adalah prosedur servis yang mengambil kerja paksa sehingga generator hanya bisa bekerja dengan nilai sinkron. Sudah terlihat jauh lebih logis, bukan?

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

, , co() , .

β€” co()

Hebat! co() , , . co()

  1. ,
  2. .next() , {done: false, value: [a Promise]}
  3. ( ), .next() ,
  4. , 4
  5. - {done: true, value: ...} , , co()

, co(), :



 function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } co(function* asyncAdds(initialValue) { console.log(yield deferred(initialValue + 1)); console.log(yield deferred(initialValue + 2)); console.log(yield deferred(initialValue + 3)); }); function co(generator) { return new Promise((resolve, reject) => { //   }); } 



, ? - 10 co() , . , . ?

– co()

, , , , co() . , .throw() .



 function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } function deferReject(e) { return new Promise((resolve, reject) => reject(e)); } co(function* asyncAdds() { console.log(yield deferred(1)); try { console.log(yield deferredError(new Error('To fail, or to not fail.'))); } catch (e) { console.log('To not fail!'); } console.log(yield deferred(3)); }); function co(generator) { return new Promise((resolve, reject) => { //   }); } 



. , , .next() onResolve() . onReject() , .throw() . try/catch , , try/catch .

, co() ! ! co() , , , . , ?

: async/await

co() . - , async/await? β€” ! , async await .

async , await , yield . await , async . async - .

, async/await , , - co() async yield await , * , .

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

:

 async function () { var user = await fetchJson('/api/user/self'); var interests = await fetchJson('/api/user/interests?userId=' + self.id); var recommendations = await Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }(); 

, :

  • co() . async , . async co() co.wrap() .
  • co() ( yield ) , , . async ( await ) .



Javascript , , Β« Β» co() , , , async/await . ? .

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


All Articles