Panduan Node.js, Bagian 6: Loop Peristiwa, Stack Panggilan, Pengatur Waktu

Hari ini, di bagian keenam dari terjemahan manual Node.js, kita akan berbicara tentang loop acara, tumpukan panggilan, fungsi process.nextTick() , dan timer. Memahami ini dan mekanisme Node.js lainnya adalah salah satu landasan pengembangan aplikasi yang sukses untuk platform ini.




Perulangan acara


Jika Anda ingin memahami bagaimana kode JavaScript dijalankan, Event Loop adalah salah satu konsep paling penting yang perlu Anda pahami. Di sini kita akan berbicara tentang cara kerja JavaScript dalam mode single-threaded, dan bagaimana fungsi asinkron ditangani.

Saya telah mengembangkan JavaScript selama bertahun-tahun, tetapi saya tidak bisa mengatakan bahwa saya benar-benar mengerti cara kerja semuanya, sehingga bisa dikatakan, "di bawah tenda". Programmer mungkin tidak menyadari seluk-beluk perangkat subsistem internal dari lingkungan di mana ia bekerja. Tetapi biasanya berguna untuk memiliki setidaknya gambaran umum tentang hal-hal seperti itu.

Kode JavaScript yang Anda tulis berjalan dalam mode single-threaded. Pada titik waktu tertentu, hanya satu tindakan yang dilakukan. Keterbatasan ini, pada kenyataannya, sangat berguna. Ini sangat menyederhanakan cara kerja program, menghilangkan kebutuhan programmer untuk memecahkan masalah khusus untuk lingkungan multi-threaded.

Faktanya, seorang programmer JS hanya perlu memperhatikan tindakan yang dilakukan kodenya, dan mencoba menghindari situasi yang menyebabkan pemblokiran utas utama. Misalnya - membuat panggilan jaringan dalam mode sinkron dan siklus tanpa akhir.

Biasanya, browser, di setiap tab yang terbuka, memiliki loop acara sendiri. Hal ini memungkinkan Anda untuk mengeksekusi kode setiap halaman dalam lingkungan yang terisolasi dan untuk menghindari situasi ketika halaman tertentu, dalam kode yang ada loop tak terbatas atau perhitungan berat dilakukan, mampu "menangguhkan" seluruh browser. Browser mendukung pekerjaan banyak loop acara yang ada secara bersamaan, digunakan, misalnya, untuk memproses panggilan ke berbagai API. Selain itu, loop acara eksklusif digunakan untuk mendukung pekerja web .

Hal terpenting yang harus selalu diingat oleh seorang programmer JavaScript adalah kodenya menggunakan loop acara sendiri, sehingga kode tersebut harus ditulis agar loop acara ini tidak diblokir.

Kunci Loop Acara


Kode JavaScript apa pun yang membutuhkan terlalu banyak waktu untuk dieksekusi, yaitu, kode yang tidak terlalu lama mengendalikan loop acara, akan memblokir eksekusi kode halaman lainnya. Ini bahkan mengarah ke pemblokiran pemrosesan peristiwa antarmuka pengguna, yang berarti bahwa pengguna tidak dapat berinteraksi dengan elemen halaman dan bekerja secara normal dengannya, misalnya, untuk menggulir.

Hampir semua mekanisme dasar JavaScript I / O bersifat non-blocking. Ini berlaku untuk browser dan Node.js. Di antara mekanisme tersebut, misalnya, kita dapat menyebutkan alat untuk melakukan permintaan jaringan yang digunakan di kedua lingkungan klien dan server, dan alat untuk bekerja dengan file Node.js. Ada metode sinkron untuk melakukan operasi seperti itu, tetapi mereka hanya digunakan dalam kasus khusus. Itulah sebabnya callback tradisional dan mekanisme yang lebih baru - janji dan konstruk async / menunggu - sangat penting dalam JavaScript.

Tumpukan panggilan


Stack JavaScript Call didasarkan pada prinsip LIFO (Last In, First Out - Last In, First Out). Perulangan acara terus-menerus memeriksa tumpukan panggilan untuk melihat apakah ia memiliki fungsi yang perlu dijalankan. Jika, ketika mengeksekusi kode, suatu fungsi dipanggil di dalamnya, informasi tentang itu ditambahkan ke tumpukan panggilan dan fungsi ini dieksekusi.

Jika bahkan sebelum Anda tidak tertarik pada konsep "tumpukan panggilan", maka jika Anda telah menemukan pesan kesalahan yang menyertakan jejak tumpukan, Anda sudah membayangkan seperti apa bentuknya. Di sini, misalnya, terlihat seperti ini di browser.


Pesan kesalahan browser

Browser, ketika terjadi kesalahan, melaporkan urutan panggilan ke fungsi, informasi tentang yang disimpan dalam tumpukan panggilan, yang memungkinkan Anda menemukan sumber kesalahan dan memahami panggilan mana ke fungsi yang menyebabkan situasi.

Sekarang kita telah berbicara tentang loop peristiwa dan tumpukan panggilan secara umum, pertimbangkan contoh yang menggambarkan eksekusi fragmen kode dan bagaimana proses ini terlihat dari sudut pandang loop peristiwa dan tumpukan panggilan.

Event Loop dan Call Stack


Berikut adalah kode yang akan kami coba:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo() 

Jika kode ini dieksekusi, berikut ini akan sampai ke konsol:

 foo bar baz 

Hasil seperti itu sangat diharapkan. Yaitu, ketika kode ini dijalankan, fungsi foo() pertama kali dipanggil. Di dalam fungsi ini, pertama-tama kita memanggil fungsi bar() , dan kemudian fungsi baz() . Pada saat yang sama, tumpukan panggilan selama eksekusi kode ini mengalami perubahan yang ditunjukkan pada gambar berikut.


Mengubah status tumpukan panggilan saat menjalankan kode

Perulangan acara, pada setiap iterasi, memeriksa untuk melihat apakah ada sesuatu di tumpukan panggilan, dan jika demikian, ia melakukannya sampai tumpukan panggilan kosong.


Iterasi loop acara

Mengantri fungsi


Contoh di atas terlihat cukup biasa, tidak ada yang istimewa tentang itu: JavaScript menemukan kode yang perlu dieksekusi dan menjalankannya secara berurutan. Kami akan berbicara tentang cara menunda eksekusi fungsi hingga tumpukan panggilan dihapus. Untuk melakukan ini, konstruksi berikut digunakan:

 setTimeout(() => {}), 0) 

Ini memungkinkan Anda untuk menjalankan fungsi yang diteruskan ke fungsi setTimeout() setelah semua fungsi lain yang disebut dalam kode program dieksekusi.

Pertimbangkan sebuah contoh:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo() 

Apa yang dicetak kode ini mungkin tampak tidak terduga:

 foo baz bar 

Ketika kita menjalankan contoh ini, fungsi foo() dipanggil terlebih dahulu. Di dalamnya, kita memanggil setTimeout() , melewati fungsi ini, sebagai argumen pertama, bar . Dengan meneruskannya 0 sebagai argumen kedua, kami memberi tahu sistem bahwa fungsi ini harus dilakukan sesegera mungkin. Kemudian kita memanggil fungsi baz() .

Beginilah tampilan tumpukan panggilan sekarang.


Mengubah status tumpukan panggilan saat menjalankan kode

Ini adalah urutan di mana fungsi-fungsi dalam program kami sekarang akan dieksekusi.


Iterasi loop acara

Mengapa ini terjadi seperti ini?

Antrian acara


Ketika fungsi setTimeout() dipanggil, browser atau platform Node.js memulai timer. Setelah penghitung waktu bekerja (dalam kasus kami, ini terjadi segera, karena kami mengaturnya ke 0), fungsi panggilan balik dilewatkan ke setTimeout() masuk ke dalam Antrian Acara.

Antrian acara, ketika datang ke browser, termasuk peristiwa yang diprakarsai oleh pengguna - peristiwa yang disebabkan oleh klik mouse pada elemen halaman, peristiwa yang dipicu ketika data dimasukkan dari keyboard. Penangan onload DOM seperti onload , fungsi yang dipanggil saat menerima jawaban untuk permintaan asinkron untuk memuat data, ada di sana. Di sini mereka menunggu giliran mereka untuk memproses.

Perulangan acara memberikan prioritas pada apa yang ada di tumpukan panggilan. Pertama, ia melakukan semua yang berhasil ditemukannya di stack, dan setelah stack kosong, ia melanjutkan untuk memproses apa yang ada di antrian acara.

Kita tidak perlu menunggu sampai fungsi seperti setTimeout() selesai bekerja, karena fungsi yang sama disediakan oleh browser dan mereka menggunakan stream mereka sendiri. Jadi, misalnya, mengatur timer selama 2 detik menggunakan fungsi setTimeout() , Anda seharusnya tidak, setelah menghentikan eksekusi kode lain, tunggu selama 2 detik ini, karena timer bekerja di luar kode Anda.

Antrian Pekerjaan ES6


ECMAScript 2015 (ES6) memperkenalkan konsep Job Queue, yang digunakan oleh janji-janji (mereka juga muncul dalam ES6). Berkat antrian pekerjaan, hasil dari mengeksekusi fungsi asinkron dapat digunakan secepat mungkin, tanpa perlu menunggu tumpukan panggilan dihapus.

Jika janji diselesaikan sebelum akhir fungsi saat ini, kode yang sesuai akan dieksekusi segera setelah fungsi saat ini selesai.

Saya menemukan analogi yang menarik untuk apa yang kita bicarakan. Ini dapat dibandingkan dengan roller coaster di taman hiburan. Setelah Anda menaiki bukit dan ingin melakukannya lagi, Anda mengambil tiket dan masuk ke dalam barisan. Inilah cara kerja antrian acara. Tetapi antrian pekerjaan terlihat berbeda. Konsep ini mirip dengan tiket diskon, yang memberi Anda hak untuk melakukan perjalanan berikutnya segera setelah Anda menyelesaikan yang sebelumnya.

Perhatikan contoh berikut:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) =>   resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo() 

Inilah yang akan menjadi output setelah eksekusi:

 foo baz should be right after baz, before bar bar 

Apa yang dapat Anda lihat di sini menunjukkan perbedaan serius antara janji (dan async / wait construct, yang didasarkan pada mereka) dan fungsi asinkron tradisional, yang pelaksanaannya diatur menggunakan setTimeout() atau API lain dari platform yang digunakan.

process.nextTick ()


Metode process.nextTick() berinteraksi dengan loop peristiwa dengan cara khusus. Kutu adalah satu siklus penuh peristiwa. Melewati fungsi ke metode process.nextTick() , kami memberi tahu sistem bahwa fungsi ini perlu dipanggil setelah iterasi saat ini dari loop acara selesai, sebelum yang berikutnya dimulai. Menggunakan metode ini terlihat seperti ini:

 process.nextTick(() => { // -  }) 

Misalkan loop acara sibuk mengeksekusi kode untuk fungsi saat ini. Ketika operasi ini selesai, mesin JavaScript akan menjalankan semua fungsi yang diteruskan ke process.nextTick() selama operasi sebelumnya. Dengan menggunakan mekanisme ini, kami berusaha untuk memastikan bahwa fungsi tertentu dijalankan secara tidak sinkron (setelah fungsi saat ini), tetapi sesegera mungkin, tanpa menempatkannya dalam antrian.

Misalnya, jika Anda menggunakan konstruksi setTimeout(() => {}, 0) , fungsi akan dieksekusi pada iterasi berikutnya dari loop acara, yaitu, jauh lebih lambat daripada ketika process.nextTick() digunakan dalam situasi yang sama. Metode ini harus digunakan ketika perlu untuk memastikan eksekusi beberapa kode pada awal iterasi berikutnya dari loop acara.

setImmediate ()


Fungsi lain yang disediakan oleh Node.js untuk eksekusi kode asinkron adalah setImmediate() . Berikut cara menggunakannya:

 setImmediate(() => { //   }) 

Fungsi panggilan balik yang dilewatkan ke setImmediate() akan dieksekusi pada iterasi berikutnya dari loop acara.

Bagaimana setImmediate() berbeda dari setTimeout(() => {}, 0) (yaitu, dari timer yang harus bekerja sesegera mungkin) dan dari process.nextTick() ?

Fungsi yang diteruskan ke process.nextTick() akan dieksekusi setelah iterasi saat ini dari loop acara telah selesai. Artinya, fungsi seperti itu akan selalu dieksekusi sebelum fungsi yang eksekusinya dijadwalkan menggunakan setTimeout() atau setImmediate() .

Memanggil fungsi setTimeout() dengan penundaan set 0 ms sangat mirip dengan memanggil setImmediate() . Urutan pelaksanaan fungsi yang ditransfer ke mereka tergantung pada berbagai faktor, tetapi dalam kedua kasus panggilan balik akan dipanggil pada iterasi berikutnya dari loop acara.

Pengatur waktu


Kami telah berbicara tentang fungsi setTimeout() , yang memungkinkan Anda untuk menjadwalkan panggilan ke panggilan balik yang dilaluinya. Mari kita luangkan waktu untuk menjelaskan lebih rinci fitur-fiturnya dan mempertimbangkan fungsi lain, setInterval() , mirip dengan itu. Dalam Node.js, fungsi untuk bekerja dengan timer termasuk dalam modul timer , tetapi Anda dapat menggunakannya tanpa menghubungkan modul ini ke dalam kode, karena itu bersifat global.

▍ fungsi setTimeout ()


Ingatlah bahwa ketika Anda memanggil fungsi setTimeout() , ia menerima panggilan balik dan waktu, dalam milidetik, setelah itu panggilan balik akan dipanggil. Pertimbangkan sebuah contoh:

 setTimeout(() => { //   2  }, 2000) setTimeout(() => { //   50  }, 50) 

Di sini kita meneruskan setTimeout() fungsi baru yang segera dijelaskan, tetapi di sini kita dapat menggunakan fungsi yang ada dengan meneruskan setTimeout() nama dan seperangkat parameter untuk menjalankannya. Ini terlihat seperti ini:

 const myFunction = (firstParam, secondParam) => { //   } //   2  setTimeout(myFunction, 2000, firstParam, secondParam) 

Fungsi setTimeout() mengembalikan pengidentifikasi waktu. Biasanya tidak digunakan, tetapi Anda dapat menyimpannya dan, jika perlu, menghapus timer jika panggilan balik yang dijadwalkan tidak lagi diperlukan:

 const id = setTimeout(() => { //      2  }, 2000) //  ,       clearTimeout(id) 

▍ Zero delay


Di bagian sebelumnya, kami menggunakan setTimeout() , meneruskannya, sebagai waktu setelah itu diperlukan untuk memanggil panggilan balik, 0 . Ini berarti bahwa panggilan balik akan dipanggil sesegera mungkin, tetapi setelah menyelesaikan fungsi saat ini:

 setTimeout(() => { console.log('after ') }, 0) console.log(' before ') 

Kode tersebut akan menampilkan yang berikut:

 before after 

Teknik ini sangat berguna dalam situasi ketika, ketika melakukan tugas komputasi yang berat, saya tidak ingin memblokir utas utama, memungkinkan fungsi lain dieksekusi, memecah tugas-tugas ini menjadi beberapa tahap, dieksekusi sebagai panggilan setTimeout() .

Jika kita mengingat fungsi setImmediate() , maka itu adalah standar di Node.js, yang tidak dapat dikatakan tentang browser (ini diterapkan di IE dan Edge, tetapi tidak pada yang lain).

▍ fungsi setInterval ()


Fungsi setInterval() mirip dengan setTimeout() , tetapi ada perbedaan di antara mereka. Alih-alih mengeksekusi callback yang diteruskan sekali, setInterval() akan secara berkala, dengan interval yang ditentukan, panggil callback ini. Ini akan berlanjut, idealnya, sampai saat ketika programmer secara eksplisit menghentikan proses ini. Berikut cara menggunakan fitur ini:

 setInterval(() => { //   2  }, 2000) 

Panggilan balik yang dilewatkan ke fungsi yang ditunjukkan di atas akan dipanggil setiap 2 detik. Untuk memberikan kemungkinan menghentikan proses ini, Anda perlu mengembalikan pengidentifikasi waktu dengan setInterval() dan menggunakan perintah clearInterval() :

 const id = setInterval(() => { //   2  }, 2000) clearInterval(id) 

Teknik umum adalah memanggil clearInterval() di dalam callback yang diteruskan ke setInterval() ketika kondisi tertentu terpenuhi. Misalnya, kode berikut akan dijalankan secara berkala hingga properti App.somethingIWait untuk arrived :

 const interval = setInterval(function() { if (App.somethingIWait === 'arrived') {   clearInterval(interval)   //    -  ,   -    } }, 100) 

▍ Pengaturan rekursif setTimeout ()


Fungsi setInterval() akan memanggil panggilan balik yang dilaluinya setiap n milidetik, tanpa khawatir apakah panggilan balik ini selesai setelah panggilan sebelumnya.

Jika setiap panggilan ke panggilan balik ini selalu membutuhkan waktu yang sama kurang dari n , maka tidak ada masalah yang muncul di sini.


Disebut callback secara berkala, setiap sesi eksekusi membutuhkan waktu yang sama, termasuk dalam interval antar panggilan

Mungkin butuh waktu berbeda untuk menyelesaikan panggilan balik, yang masih kurang dari n . Jika, misalnya, kita berbicara tentang melakukan operasi jaringan tertentu, maka situasi ini sangat diharapkan.


Disebut callback secara berkala, setiap sesi pelaksanaannya membutuhkan waktu yang berbeda, jatuh di antara panggilan

Saat menggunakan setInterval() , situasi dapat muncul ketika panggilan balik mengambil lebih dari n , yang mengarah ke panggilan berikutnya selesai sebelum yang sebelumnya selesai.


Disebut callback secara berkala, setiap sesi membutuhkan waktu yang berbeda, yang terkadang tidak sesuai dengan interval antar panggilan

Untuk menghindari situasi ini, Anda dapat menggunakan teknik pengaturan pengatur waktu rekursif menggunakan setTimeout() . Intinya adalah bahwa panggilan panggil berikutnya direncanakan setelah selesainya panggilan sebelumnya:

 const myFunction = () => { //    setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) 

Dengan pendekatan ini, skenario berikut dapat diimplementasikan:


Panggilan rekursif ke setTimeout () untuk menjadwalkan eksekusi panggilan balik

Ringkasan


Hari ini kita berbicara tentang mekanisme internal Node.js, seperti loop acara, tumpukan panggilan, dan membahas bekerja dengan timer yang memungkinkan Anda untuk menjadwalkan eksekusi kode. Lain kali kita akan mempelajari topik pemrograman asinkron.

Pembaca yang budiman! Pernahkah Anda mengalami situasi ketika Anda harus menggunakan process.nextTick ()?

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


All Articles