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 nerakaAwalnya, 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?
JanjiJanji 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):
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:
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 AsinkronGenerator 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.
GeneratorDalam 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());
Ini cukup sederhana, tapi bagaimanapun, mari kita bicara tentang apa yang terjadi di sini:
const counter = counts();
- inisialisasi generator dan simpan di penghitung variabel. Generator ada dalam limbo, belum ada kode di tubuh generator yang dieksekusi.console.log(counter.next());
- Interpretasi output ( yield
) 1, setelah itu 1 dikembalikan sebagai value
, dan done
menghasilkan false
, karena output tidak berakhir di sanaconsole.log(counter.next());
- Sekarang 2!console.log(counter.next());
- Sekarang 3! Selesai Apakah semuanya benar? Tidak. Eksekusi dijeda dalam langkah yield 3;
Untuk menyelesaikan, Anda perlu menelepon next () lagi.console.log(counter.next());
- Sekarang 4, dan ia kembali, tetapi tidak dikeluarkan, jadi sekarang kita keluar dari fungsinya, dan semuanya siap.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);
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 generatorOke, 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());
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());
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);
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());
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());
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());
Masalah Pemrograman - Penetrasi Kesalahan GeneratorKamerad, 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
PolaSolusiBoom! 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 korutinSekarang 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()
- ,
.next()
, {done: false, value: [a Promise]}
- ( ),
.next()
, - , 4
- -
{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/awaitco()
. - , 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
. ? .