Pemrograman asinkron dalam JavaScript (Callback, Promise, RxJs)
Halo semua. Berhubungan dengan Omelnitsky Sergey. Belum lama ini, saya mengadakan streaming tentang pemrograman reaktif, di mana saya berbicara tentang asinkroni dalam JavaScript. Hari ini saya ingin merangkum materi ini.
Namun sebelum kita memulai materi utamanya, kita perlu membuat pendahuluan. Jadi mari kita mulai dengan definisi: apa itu stack dan queue?
Tumpukan adalah koleksi yang elemennya diambil berdasarkan LIFO "masuk terakhir, keluar pertama".
Antrian adalah kumpulan yang unsur-unsurnya diperoleh menurut prinsip FIFO (βmasuk pertama, keluar pertamaβ.
Oke, mari kita lanjutkan.
JavaScript adalah bahasa pemrograman single-threaded. Ini berarti bahwa ia hanya memiliki satu utas eksekusi dan satu tumpukan tempat fungsi diantrekan untuk dieksekusi. Oleh karena itu, JavaScript hanya dapat melakukan satu operasi pada satu waktu, sementara operasi lain akan menunggu giliran mereka di tumpukan hingga dipanggil.
tumpukan panggilan adalah struktur data yang, secara sederhana, mencatat informasi tentang tempat di program dimana kita berada. Jika kita melompat ke suatu fungsi, kita mendorong entri tersebut ke bagian atas tumpukan. Saat kita kembali dari suatu fungsi, kita mengeluarkan elemen paling atas dari tumpukan dan berakhir di tempat kita memanggil fungsi ini. Hanya itu yang bisa dilakukan tumpukan. Dan sekarang pertanyaan yang sangat menarik. Lalu bagaimana cara kerja asinkron di JavasScript?
Faktanya, selain tumpukan, browser memiliki antrian khusus untuk bekerja dengan apa yang disebut WebAPI. Fungsi dari antrian ini akan dijalankan secara berurutan hanya setelah tumpukan dibersihkan sepenuhnya. Hanya setelah itu mereka ditempatkan dari antrian ke tumpukan untuk dieksekusi. Jika saat ini ada setidaknya satu elemen di tumpukan, maka elemen tersebut tidak dapat masuk ke tumpukan. Hanya karena ini, memanggil fungsi dengan batas waktu seringkali tidak akurat dalam waktu, karena fungsi tidak dapat berpindah dari antrian ke tumpukan saat penuh.
Mari kita lihat contoh berikut dan lalui langkah demi langkah. Mari kita lihat juga apa yang terjadi di sistem.
1) Sejauh ini tidak terjadi apa-apa. Konsol browser bersih, tumpukan panggilan kosong.
2) Kemudian perintah console.log('Hi') ditambahkan ke tumpukan panggilan.
3) Dan itu terpenuhi
4) Kemudian console.log('Hi') dihapus dari tumpukan panggilan.
5) Sekarang mari kita beralih ke perintah setTimeout(function cb1() {β¦ }). Itu ditambahkan ke tumpukan panggilan.
6) Perintah setTimeout(function cb1() {β¦ }) dijalankan. Browser membuat pengatur waktu yang merupakan bagian dari Web API. Ini akan melakukan hitungan mundur.
7) Perintah setTimeout(function cb1() {β¦ }) telah menyelesaikan tugasnya dan dihapus dari tumpukan panggilan.
8) Perintah console.log('Bye') ditambahkan ke tumpukan panggilan.
9) Perintah console.log('Bye') dijalankan.
10) Perintah console.log('Bye') dihapus dari tumpukan panggilan.
11) Setelah setidaknya 5000ms berlalu, pengatur waktu berakhir dan menempatkan callback cb1 ke dalam antrean callback.
12) Perulangan peristiwa mengambil fungsi cb1 dari antrian panggilan balik dan mendorongnya ke tumpukan panggilan.
13) Fungsi cb1 dijalankan dan menambahkan console.log('cb1') ke tumpukan panggilan.
14) Perintah console.log('cb1') dijalankan.
15) Perintah console.log('cb1') dihapus dari tumpukan panggilan.
16) Fungsi cb1 dihapus dari tumpukan panggilan.
Mari kita lihat contoh dalam dinamika:
Ya, kita melihat bagaimana asinkroni diterapkan dalam JavaScript. Sekarang mari kita bicara secara singkat tentang evolusi kode asynchronous.
Evolusi kode asinkron.
a(function (resultsFromA) {
b(resultsFromA, function (resultsFromB) {
c(resultsFromB, function (resultsFromC) {
d(resultsFromC, function (resultsFromD) {
e(resultsFromD, function (resultsFromE) {
f(resultsFromE, function (resultsFromF) {
console.log(resultsFromF);
})
})
})
})
})
});
Pemrograman asinkron seperti yang kita kenal di JavaScript hanya dapat dilakukan dengan fungsi. Mereka dapat diteruskan seperti variabel lainnya ke fungsi lainnya. Beginilah cara callback lahir. Dan asik, asyik dan bergairah, hingga berubah menjadi sedih, melankolis, dan sedih. Mengapa? Ya, itu sederhana:
Seiring bertambahnya kompleksitas kode, proyek dengan cepat berubah menjadi beberapa blok bersarang yang tidak jelas - βcallback hellβ.
Penanganan kesalahan dapat dengan mudah diabaikan.
Anda tidak dapat mengembalikan ekspresi dengan return.
Dengan munculnya Promise, situasinya menjadi sedikit lebih baik.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 2000);
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
throw new Error('FAILED HERE');
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
}).catch((e) => {
console.log('error: ', e);
});
Rantai janji muncul, yang meningkatkan keterbacaan kode
Ada metode intersepsi kesalahan yang terpisah
Eksekusi paralel dengan Promise.all ditambahkan
Kita dapat menyelesaikan asinkroni bersarang dengan async/menunggu
Namun janji tersebut mempunyai keterbatasan. Misalnya, sebuah janji, tanpa menari dengan rebana, tidak dapat dibatalkan, dan yang terpenting, itu berhasil dengan satu nilai.
Nah, di sini kita dengan lancar mendekati pemrograman reaktif. Lelah? Hal baiknya adalah, Anda bisa membuat minuman, bertukar pikiran, dan kembali untuk membaca lebih lanjut. Dan saya akan melanjutkan.
Pemrograman reaktifβ- paradigma pemrograman yang berfokus pada aliran data dan penyebaran perubahan. Mari kita lihat lebih dekat apa itu aliran data.
Bayangkan kita memiliki kolom input. Kami membuat sebuah array, dan untuk setiap keyup dari event input, kami akan menyimpan event tersebut di array kami. Pada saat yang sama, saya ingin mencatat bahwa array kami diurutkan berdasarkan waktu, mis. indeks peristiwa-peristiwa yang terjadi kemudian lebih besar daripada indeks peristiwa-peristiwa sebelumnya. Array seperti itu adalah model aliran data yang disederhanakan, namun belum menjadi aliran. Agar larik ini dapat disebut aliran dengan aman, larik ini harus dapat memberi tahu pelanggan bahwa data baru telah tiba di dalamnya. Jadi kita sampai pada definisi aliran.
ΠΠΎΡΠΎΠΊβadalah larik data yang diurutkan berdasarkan waktu yang dapat menunjukkan bahwa data telah berubah. Sekarang bayangkan betapa mudahnya menulis kode di mana Anda perlu memicu beberapa peristiwa di berbagai bagian kode untuk satu tindakan. Kami cukup berlangganan aliran dan itu akan memberi tahu kami ketika perubahan terjadi. Dan perpustakaan RxJs dapat melakukan ini.
RxJS adalah perpustakaan untuk bekerja dengan program asinkron dan berbasis peristiwa menggunakan urutan yang dapat diamati. Perpustakaan menyediakan tipe utama Tampak, beberapa tipe pembantu (Pengamat, Penjadwal, Subyek) dan operator untuk bekerja dengan acara seperti halnya koleksi (petakan, filter, kurangi, setiap dan yang serupa dari JavaScript Array).
Mari kita pahami konsep dasar perpustakaan ini.
Yang Dapat Diamati, Pengamat, Produser
Observable adalah tipe dasar pertama yang akan kita lihat. Kelas ini berisi bagian utama dari implementasi RxJs. Ini terkait dengan aliran yang dapat diamati, yang dapat dilanggan menggunakan metode berlangganan.
Observable mengimplementasikan mekanisme tambahan untuk membuat pembaruan, yang disebut Pengamat. Sumber nilai bagi Pengamat disebut Produsen. Itu bisa berupa array, iterator, soket web, semacam acara, dll. Jadi bisa dikatakan observable adalah konduktor antara Produser dan Observer.
Observable menangani tiga jenis peristiwa Pengamat:
selanjutnya - data baru
error - kesalahan jika urutan dihentikan karena pengecualian. peristiwa ini juga menyiratkan akhir dari rangkaian tersebut.
selesai - sinyal tentang akhir urutan. Artinya tidak akan ada lagi data baru
Mari kita lihat demonya:
Pada awalnya kita akan memproses nilai 1, 2, 3, dan setelah 1 detik. kami mendapatkan 4 dan mengakhiri utas kami.
Berpikir keras
Dan kemudian saya menyadari bahwa menceritakannya lebih menarik daripada menulisnya. π
Subscription
Saat kami berlangganan aliran, kami membuat kelas baru berlangganan, yang memberi kita opsi untuk berhenti berlangganan dengan metode ini unsubscribe. Kami juga dapat mengelompokkan langganan menggunakan metode ini menambahkan. Ya, logis jika kita dapat memisahkan grup thread menggunakan menghapus. Metode tambah dan hapus menerima langganan berbeda sebagai masukan. Saya ingin mencatat bahwa ketika kami berhenti berlangganan, kami berhenti berlangganan dari semua langganan anak seolah-olah mereka juga disebut metode berhenti berlangganan. Teruskan.
Jenis aliran
PANAS
DINGIN
Produser diciptakan di luar yang dapat diamati
Produser dibuat di dalam yang dapat diamati
Data diteruskan pada saat observasi dibuat
Data disediakan pada saat berlangganan.
Perlu lebih banyak logika untuk berhenti berlangganan
Utas berakhir dengan sendirinya
Menggunakan hubungan satu ke banyak
Menggunakan hubungan satu lawan satu
Semua langganan memiliki nilai yang sama
Langganan bersifat independen
Data bisa hilang jika tidak ada langganan
Menerbitkan ulang semua nilai aliran untuk langganan baru
Sebagai analogi, saya membayangkan streaming panas seperti film di bioskop. Pada jam berapa Anda datang, sejak saat itu Anda mulai menonton. Saya akan membandingkan aliran dingin dengan panggilan di dalamnya. mendukung. Setiap penelepon mendengarkan rekaman mesin penjawab dari awal sampai akhir, tetapi Anda dapat menutup telepon dengan berhenti berlangganan.
Saya ingin mencatat bahwa ada juga yang disebut aliran hangat (saya sangat jarang menemukan definisi seperti itu dan hanya di komunitas asing) - ini adalah aliran yang berubah dari aliran dingin menjadi aliran panas. Timbul pertanyaan - di mana menggunakannya)) Saya akan memberikan contoh dari latihan.
Saya bekerja dengan Angular. Dia aktif menggunakan rxjs. Untuk mendapatkan data ke server, saya mengharapkan aliran dingin dan saya menggunakan aliran ini di template menggunakan asyncPipe. Jika saya menggunakan pipa ini beberapa kali, lalu kembali ke definisi aliran dingin, setiap pipa akan meminta data dari server, yang paling tidak aneh. Dan jika saya mengubah aliran dingin menjadi aliran hangat, maka permintaan akan terkabul satu kali.
Secara umum memahami jenis aliran cukup sulit bagi pemula, namun penting.
Operator
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);
Operator memberi kami kesempatan untuk bekerja dengan aliran. Mereka membantu mengendalikan peristiwa yang mengalir di Observable. Kami akan mempertimbangkan beberapa yang paling populer, dan informasi lebih lanjut tentang operator dapat ditemukan di tautan informasi berguna.
Operator-dari
Mari kita mulai dengan operator pembantu. Ini menciptakan Observable berdasarkan nilai sederhana.
Filter operator
Operator filter, seperti namanya, memfilter sinyal aliran. Jika operator mengembalikan nilai true, maka operator akan melompat lebih jauh.
Operator - ambil
take - Mengambil nilai jumlah emisi, setelah itu aliran berakhir.
Operator-debounceTime
debounceTime - membuang nilai yang dipancarkan yang berada dalam interval waktu yang ditentukan antara data keluaran - setelah interval waktu berlalu, memancarkan nilai terakhir.
Operator gabungangabungkanLatest agak mirip dengan janji.semua. Ini menggabungkan beberapa aliran menjadi satu. Setelah setiap utas membuat setidaknya satu emisi, kami mendapatkan nilai terbaru dari masing-masing utas sebagai array. Selanjutnya, setelah ada emisi dari aliran gabungan, itu akan memberikan nilai baru.
Zip - menunggu nilai dari setiap aliran dan membentuk larik berdasarkan nilai ini. Jika nilai tidak berasal dari thread manapun, maka grup tidak akan terbentuk.
Operator tap memungkinkan Anda melakukan efek samping, yaitu tindakan apa pun yang tidak memengaruhi urutan.
Operator utilitas berbagi dapat mengubah aliran dingin menjadi aliran panas.
Operator sudah selesai. Mari beralih ke Subjek.
Berpikir keras
Dan kemudian saya pergi minum teh. Saya bosan dengan contoh-contoh ini π
Keluarga subjek
Kelompok subjek adalah contoh utama dari topik hangat. Kelas-kelas ini adalah sejenis hibrida yang bertindak sebagai yang dapat diamati dan menjadi pengamat pada saat yang bersamaan. Karena subjeknya adalah aliran panas, maka harus berhenti berlangganan. Jika kita berbicara tentang metode utama, maka ini adalah:
selanjutnya - meneruskan data baru ke aliran
kesalahan - kesalahan dan penghentian utas
selesai - akhir utas
berlangganan - berlangganan aliran
berhenti berlangganan - berhenti berlangganan aliran
asObservable - berubah menjadi pengamat
toPromise - berubah menjadi janji
Alokasikan 4 5 jenis mata pelajaran.
Berpikir keras
Saya mengatakan 4 di aliran, tetapi ternyata mereka menambahkan satu lagi. Seperti kata pepatah, hidup dan belajar.
Subjek Sederhana new Subject()- jenis mata pelajaran yang paling sederhana. Dibuat tanpa parameter. Melewati nilai yang datang hanya setelah berlangganan.
Subyek Perilaku new BehaviorSubject( defaultData<T> ) - menurut saya jenis subjek yang paling umum. Input mengambil nilai default. Selalu menyimpan data edisi terakhir yang dikirimkan saat berlangganan. Kelas ini juga memiliki metode nilai berguna yang mengembalikan nilai aliran saat ini.
Putar Ulang Subjek new ReplaySubject(bufferSize?: number, windowTime?: number) - Secara opsional, argumen pertama dapat mengambil ukuran buffer nilai yang akan disimpannya sendiri, dan kedua kalinya kita memerlukan perubahan.
subjek async new AsyncSubject() - tidak ada yang terjadi saat berlangganan, dan nilainya hanya akan dikembalikan setelah selesai. Hanya nilai aliran terakhir yang akan dikembalikan.
Subjek WebSocket new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Dokumentasinya tidak menyebutkan hal itu dan saya sendiri melihatnya untuk pertama kalinya. Siapa yang tahu apa yang dia lakukan, tulis, kami akan menambahkan.
Fiuh. Baiklah, kami telah mempertimbangkan semua yang ingin saya sampaikan hari ini. Semoga informasi ini bermanfaat. Anda dapat membaca sendiri daftar literatur di tab Informasi Berguna.