Pemrograman JavaScript Asinkron (Panggilan Balik, Janji, RxJ)

Halo semuanya. Sentuhan Omelnitsky Sergey. Belum lama ini saya memimpin aliran pemrograman reaktif di mana saya berbicara tentang asinkron dalam JavaScript. Hari ini saya ingin menguraikan materi ini.



Tetapi sebelum kita memulai materi utama, kita perlu melakukan yang pengantar. Jadi, mari kita mulai dengan definisi: apa tumpukan dan antrian?


Tumpukan adalah kumpulan yang elemen-elemennya diterima dengan prinsip LIFO "last in, first out"


Antrian adalah kumpulan yang unsur-unsurnya diterima sesuai dengan prinsip (FIFO pertama masuk, pertama keluar)


Ok, mari kita lanjutkan.



JavaScript adalah bahasa pemrograman single-threaded. Ini berarti bahwa ia hanya memiliki satu utas eksekusi dan satu tumpukan di mana fungsi-fungsi di-antri untuk dieksekusi. Oleh karena itu, pada satu titik waktu, JavaScript hanya dapat melakukan satu operasi, sementara operasi lain akan menunggu giliran mereka di stack hingga dipanggil.


Tumpukan panggilan adalah struktur data yang, secara sederhana, mencatat informasi tentang tempat dalam program di mana kita berada. Jika kita masuk ke suatu fungsi, kita mencatatnya di bagian atas tumpukan. Ketika kita kembali dari fungsi, kita menarik elemen paling atas dari tumpukan dan menemukan diri kita dari mana kita memanggil fungsi ini. Ini semua yang bisa dilakukan tumpukan. Dan sekarang pertanyaan yang sangat menarik. Lalu bagaimana cara kerja asinkron dalam JavasScript?



Bahkan, selain stack, browser memiliki antrian khusus untuk bekerja dengan WebAPI. Fungsi dari antrian ini akan dieksekusi hanya setelah tumpukan benar-benar dihapus. Hanya setelah itu mereka didorong dari antrian ke tumpukan untuk dieksekusi. Jika setidaknya satu elemen saat ini di tumpukan, maka mereka tidak bisa masuk ke tumpukan. Justru karena ini fungsi panggilan oleh timeout sering tidak akurat dalam waktu, karena fungsi tidak dapat pergi dari antrian ke tumpukan saat penuh.


Pertimbangkan contoh berikut dan lakukan "eksekusi" langkah demi langkah. Lihat juga apa yang terjadi di sistem.


console.log('Hi'); setTimeout(function cb1() { console.log('cb1'); }, 5000); console.log('Bye'); 


1) Sejauh ini, tidak ada yang terjadi. Konsol browser bersih, tumpukan panggilan kosong.



2) Kemudian perintah console.log ('Hai') ditambahkan ke tumpukan panggilan.



3) Dan itu dieksekusi



4) Kemudian console.log ('Hai') dihapus dari tumpukan panggilan.



5) Sekarang buka perintah setTimeout (function cb1 () {...}). Itu ditambahkan ke tumpukan panggilan.



6) Perintah setTimeout (function cb1 () {...}) dijalankan. Browser membuat timer yang merupakan bagian dari Web API. Dia akan melakukan hitung mundur.



7) Perintah setTimeout (fungsi cb1 () {...}) telah selesai 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 5000 ms berlalu, timer keluar dan menempatkan callback cb1 dalam antrian callback.



12) Loop acara mengambil fungsi cb1 dari antrian panggilan balik dan menempatkannya di 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.


Lihatlah contoh dalam dinamika:



Nah, di sini kita telah memeriksa bagaimana asinkron diimplementasikan dalam JavaScript. Sekarang mari kita bicara secara singkat tentang evolusi kode asinkron.


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 dalam JavaScript, hanya dapat diimplementasikan dengan fungsi. Mereka dapat diteruskan seperti variabel lain ke fungsi lainnya. Jadi callback lahir. Dan itu keren, menyenangkan dan provokatif, sampai berubah menjadi kesedihan, kerinduan dan kesedihan. Mengapa Ya, semuanya sederhana:


  • Dengan meningkatnya kompleksitas kode, proyek dengan cepat berubah menjadi blok berulang kali tersembunyi - "neraka panggilan balik".
  • Penanganan kesalahan dapat dengan mudah dilewatkan.
  • Anda tidak dapat mengembalikan ekspresi dengan return.

Dengan Janji, segalanya 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
  • Metode perangkap kesalahan terpisah telah muncul
  • Sekarang Anda dapat berjalan secara paralel menggunakan Promise.all
  • Kita dapat memecahkan asynchrony bersarang dengan async / menunggu

Tetapi promis memiliki keterbatasan. Misalnya, sebuah janji, tanpa menari dengan rebana, tidak dapat dibatalkan, dan yang terpenting, itu bekerja dengan satu nilai.


Yah, kami dengan lancar mendekati pemrograman reaktif. Apakah kamu lelah? Nah, hal baiknya adalah Anda dapat membuat beberapa camar, memikirkan kembali dan kembali untuk membaca lebih lanjut. Dan saya akan melanjutkan.


Dasar-Dasar Pemrograman Reaktif


Pemrograman reaktif adalah paradigma pemrograman yang berfokus pada aliran data dan penyebaran perubahan. Mari kita lihat lebih dekat apa itu aliran data.


 //     const input = ducument.querySelector('input'); const eventsArray = []; //      eventsArray input.addEventListener('keyup', event => eventsArray.push(event) ); 

Bayangkan bahwa kita memiliki bidang input. Kami membuat array, dan untuk setiap keyup acara input, kami akan menyimpan acara di array kami. Dalam hal ini, saya ingin mencatat bahwa array kami diurutkan berdasarkan waktu yaitu indeks peristiwa kemudian lebih besar dari indeks yang sebelumnya. Array seperti itu adalah model aliran data yang disederhanakan, tetapi ini belum merupakan aliran. Agar array ini dapat disebut stream dengan aman, ia harus entah bagaimana dapat memberi tahu pelanggan bahwa ia telah menerima data baru. Jadi kita sampai pada definisi aliran.


Aliran data


 const { interval } = Rx; const { take } = RxOperators; interval(1000).pipe( take(4) ) 


Aliran adalah array data yang diurutkan berdasarkan waktu yang dapat menunjukkan bahwa data telah berubah. Sekarang bayangkan betapa nyamannya menulis kode di mana Anda perlu memicu beberapa peristiwa di berbagai bagian kode dalam satu tindakan. Kami hanya berlangganan aliran dan dia akan memberi tahu kami ketika perubahan terjadi. Dan perpustakaan RxJ dapat melakukan ini.



RxJS adalah perpustakaan untuk bekerja dengan program asinkron dan berbasis acara menggunakan urutan yang dapat diamati. Perpustakaan menyediakan jenis utama dari Observable , beberapa jenis tambahan ( Pengamat, Penjadwal, Subjek ) dan operator yang bekerja dengan acara seperti dengan koleksi ( peta, filter, pengurangan, setiap dan sejenisnya dari JavaScript Array).


Mari kita lihat konsep dasar perpustakaan ini.


Diamati, Pengamat, Produser


Dapat diamati adalah tipe dasar pertama yang akan kita lihat. Kelas ini berisi sebagian besar implementasi RxJs. Ini dikaitkan dengan aliran yang dapat diamati, yang Anda dapat berlangganan menggunakan metode berlangganan.


Observable mengimplementasikan mekanisme bantu untuk membuat pembaruan, yang disebut Pengamat . Sumber nilai untuk Pengamat disebut Produser . Ini bisa berupa array, iterator, soket web, semacam acara, dll. Jadi kita dapat mengatakan bahwa yang dapat diamati adalah konduktor antara Produser dan Pengamat.


Dapat diamati menangani tiga jenis peristiwa Pengamat:


  • selanjutnya - data baru
  • kesalahan - kesalahan jika urutan berakhir karena pengecualian. Acara ini juga melibatkan penyelesaian urutan.
  • selesai - sinyal tentang penyelesaian urutan. Ini berarti tidak akan ada data baru.

Mari kita lihat demo:



Pada awalnya kita akan memproses nilai 1, 2, 3, dan setelah 1 detik. kita akan mendapatkan 4 dan mengakhiri aliran kita.


Pikiran di telinga

Dan kemudian saya menyadari bahwa bercerita lebih menarik daripada menulis tentang itu. : D


Berlangganan


Saat kami berlangganan aliran, kami membuat kelas berlangganan baru yang memungkinkan kami untuk berhenti berlangganan menggunakan metode berhenti berlangganan . Kami juga dapat mengelompokkan langganan menggunakan metode add . Yah, masuk akal bahwa kita dapat memisahkan thread dengan menghapus . Metode input tambah dan hapus menerima langganan lain. Saya ingin mencatat bahwa ketika kami berhenti berlangganan, kami berhenti berlangganan dari semua langganan anak seolah-olah mereka memanggil metode berhenti berlangganan. Silakan.


Jenis aliran


PanasDINGIN
Produser yang dibuat di luar dapat diamatiProduser yang dibuat di dalam diamati
Data ditransfer pada saat diamati diamati dibuat.Data dilaporkan pada saat berlangganan
Perlu lebih banyak logika untuk berhenti berlanggananUtas berakhir dengan sendirinya
Menggunakan komunikasi satu-ke-banyakMenggunakan hubungan satu-ke-satu
Semua langganan memiliki nilai yang sama.Langganan bersifat independen
Data dapat hilang jika tidak ada langgananMenerbitkan ulang semua nilai streaming untuk langganan baru

Sebagai analogi, saya membayangkan aliran panas seperti film di bioskop. Pada titik waktu Anda datang, sejak saat itu dan mulai melihat. Saya akan membandingkan aliran dingin dengan panggilan di dalamnya. dukungan. Setiap penelepon mendengarkan mesin penjawab dari awal hingga selesai, tetapi Anda dapat menutup telepon dengan berhenti berlangganan.


Saya ingin mencatat bahwa masih ada yang disebut aliran hangat (definisi yang sangat jarang saya temui dan hanya di komunitas asing) - ini adalah aliran yang mengubah dari aliran dingin menjadi panas. Timbul pertanyaan - di mana untuk menggunakan)) Saya akan memberikan contoh dari latihan.


Saya bekerja dengan sudut. Dia aktif menggunakan rxjs. Untuk mendapatkan data ke server, saya mengharapkan aliran dingin dan saya menggunakan aliran ini di templat menggunakan asyncPipe. Jika saya menggunakan pipa ini beberapa kali, maka, kembali ke definisi aliran dingin, setiap pipa akan meminta data dari server, yang aneh untuk sedikitnya. Dan jika saya mengubah aliran dingin menjadi hangat, maka permintaan akan terjadi sekali.


Secara umum, memahami bentuk arus cukup rumit untuk pemula, tetapi 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 kemampuan untuk bekerja dengan stream. Mereka membantu mengendalikan peristiwa yang terjadi di Observable. Kami akan mempertimbangkan beberapa yang paling populer, dan operator dapat ditemukan lebih detail menggunakan tautan dalam informasi yang bermanfaat.


Operator - dari


Kami mulai dengan operator tambahan. Itu menciptakan diamati berdasarkan nilai sederhana.



Operator - filter



Filter operator filter, sesuai namanya, menyaring sinyal aliran. Jika operator mengembalikan nilai true, maka lompati lebih jauh.


Operator - ambil



take - Mengambil nilai jumlah emisi, setelah itu aliran berakhir.


Operator - debounceTime



debounceTime - membuang nilai yang dipancarkan yang termasuk dalam periode waktu yang ditentukan antara data output - setelah selang waktu interval itu memancarkan nilai terakhir.


 const { Observable } = Rx; const { debounceTime, take } = RxOperators; Observable.create((observer) => { let i = 1; observer.next(i++); //     1000 setInterval(() => { observer.next(i++) }, 1000); //     1500 setInterval(() => { observer.next(i++) }, 1500); }).pipe( debounceTime(700), //  700     take(3) ); 


Operator - takeWhile



Ini memancarkan nilai sampai takeWhile mengembalikan false, setelah itu akan berhenti berlangganan dari aliran.


 const { Observable } = Rx; const { debounceTime, takeWhile } = RxOperators; Observable.create((observer) => { let i = 1; observer.next(i++); //     1000 setInterval(() => { observer.next(i++) }, 1000); }).pipe( takeWhile( producer => producer < 5 ) ); 


Operator - gabungkanLatest


Operator CombateLatest agak mirip dengan janji.semua. Ini menggabungkan beberapa utas menjadi satu. Setelah setiap utas membuat setidaknya satu emisi, kita mendapatkan nilai terakhir dari masing-masing dalam bentuk array. Selanjutnya, setelah memancarkan apa pun dari arus gabungan, itu akan memberikan nilai baru.



 const { combineLatest, Observable } = Rx; const { take } = RxOperators; const observer_1 = Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next('a: ' + i++); }, 1000); }); const observer_2 = Observable.create((observer) => { let i = 1; //     750 setInterval(() => { observer.next('b: ' + i++); }, 750); }); combineLatest(observer_1, observer_2).pipe(take(5)); 


Operator - zip


Zip - menunggu nilai dari setiap aliran dan membentuk array berdasarkan nilai-nilai ini. Jika nilai tidak berasal dari aliran apa pun, maka grup tidak akan terbentuk.



 const { zip, Observable } = Rx; const { take } = RxOperators; const observer_1 = Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next('a: ' + i++); }, 1000); }); const observer_2 = Observable.create((observer) => { let i = 1; //     750 setInterval(() => { observer.next('b: ' + i++); }, 750); }); const observer_3 = Observable.create((observer) => { let i = 1; //     500 setInterval(() => { observer.next('c: ' + i++); }, 500); }); zip(observer_1, observer_2, observer_3).pipe(take(5)); 


Operator - forkJoin


forkJoin juga merangkai utas, tetapi hanya nilai saat semua utas selesai.



 const { forkJoin, Observable } = Rx; const { take } = RxOperators; const observer_1 = Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next('a: ' + i++); }, 1000); }).pipe(take(3)); const observer_2 = Observable.create((observer) => { let i = 1; //     750 setInterval(() => { observer.next('b: ' + i++); }, 750); }).pipe(take(5)); const observer_3 = Observable.create((observer) => { let i = 1; //     500 setInterval(() => { observer.next('c: ' + i++); }, 500); }).pipe(take(4)); forkJoin(observer_1, observer_2, observer_3); 


Operator - peta


Operator transformasi peta mengkonversi nilai emisi ke yang baru.



 const { Observable } = Rx; const { take, map } = RxOperators; Observable.create((observer) => { let i = 1; //     1000 setInterval(() => { observer.next(i++); }, 1000); }).pipe( map(x => x * 10), take(3) ); 


Operator - bagikan, ketuk


Operator keran - memungkinkan Anda melakukan efek samping, yaitu tindakan apa pun yang tidak memengaruhi urutannya.


Operator berbagi utilitas dapat membuatnya panas dari aliran dingin.



Dengan operator selesai. Mari beralih ke Subjek.


Pikiran di telinga

Dan kemudian saya pergi minum beberapa burung camar. Contoh-contoh ini membuat saya bosan: D


Keluarga subjek


Keluarga subjek adalah contoh utama dari aliran panas. Kelas-kelas ini adalah jenis hibrida yang bertindak secara simultan sebagai pengamat dan pengamat. Karena subjek adalah aliran panas, Anda harus berhenti berlangganan darinya. Jika kita berbicara tentang metode dasar, maka ini:


  • selanjutnya - transfer data baru ke stream
  • kesalahan - kesalahan dan penghentian aliran
  • lengkap - penghentian aliran
  • berlangganan - berlangganan aliran
  • berhenti berlangganan - berhenti berlangganan dari aliran
  • asObservable - berubah menjadi pengamat
  • toPromise - berubah menjadi janji

Alokasikan 4 5 jenis subjek.


Pikiran di telinga

Dia berbicara di sungai 4, tetapi ternyata mereka menambahkan satu lagi. Seperti kata pepatah, hidup dan belajar.


Subjek Sederhana Subjek new Subject() adalah jenis subjek yang paling sederhana. Itu dibuat tanpa parameter. Melewati nilai yang datang hanya setelah berlangganan.


BehaviorSubject new BehaviorSubject( defaultData<T> ) - menurut saya jenis subjek yang paling umum. Input menerima nilai default. Itu selalu menyimpan data dari emisi terakhir, yang ditransfer saat berlangganan. Kelas ini juga memiliki metode nilai berguna yang mengembalikan nilai arus stream.


ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - Input opsional dapat menerima argumen pertama sebagai ukuran buffer nilai yang akan disimpan dalam dirinya sendiri, dan kedua kalinya selama yang kita perlu perubahan.


AsyncSubject new AsyncSubject() - tidak ada yang terjadi ketika berlangganan, dan nilai akan dikembalikan hanya setelah selesai. Hanya nilai streaming terakhir yang akan dikembalikan.


WebSocketSubject WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Dokumentasi diam tentang hal itu dan saya melihatnya untuk pertama kali. Siapa yang tahu apa yang dia lakukan, tulis, tambah.


Fuf. Nah, di sini kita telah mempertimbangkan semua yang ingin saya sampaikan hari ini. Saya harap informasi ini bermanfaat. Anda dapat membiasakan diri dengan daftar referensi di tab informasi yang berguna.


Informasi yang Berguna


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


All Articles