Event Loop Pada JavaScript
Sebelum artikel ini kita sudah membahas Single Thread dan operasi non-blocking pada JavaScript. Kemudian kita juga sudah mengetahui bahwa operasi non-blocking dan kemampuan concurrency pada JavaScript di dukung oleh sebuah mekanisme bernama Event Loop.
Pada ilustrasi diatas kita bisa melihat beberapa komponen yang saling terhubung seperti JS Engine, Web APIs, callback queue dan Event Loop itu sendiri.
Apa fungsi masing-masing dari komponen tersebut? mari kita bahas satu per satu.
Call Stack
Call stack adalah struktur data yang mengikuti prinsip Last In First Out (LIFO), artinya elemen terakhir yang dimasukkan ke dalam stack akan dieksekusi atau dikeluarkan terlebih dahulu. Fungsinya sangat penting:
Mengatur urutan eksekusi kode dalam program.
Melacak titik kembali setelah fungsi selesai dieksekusi.
Mengelola alokasi memori untuk variabel lokal.
Untuk memahami cara kerja call stack, mari kita lihat contoh kode berikut:
function firstFunction() {
console.log('First Function');
secondFunction();
console.log('First Function End');
}
function secondFunction() {
console.log('Second Function');
thirdFunction();
console.log('Second Function End');
}
function thirdFunction() {
console.log('Third Function');
}
firstFunction();
Apa yang terjadi pada call stack?
Call Stack (Kosong) |
- Pada saat program dijalankan, call stack masih kosong. Global execution context atau
main()
akan dipanggil, kemudian sebuah frame baru akan ditambahkan ke call stack.
Call Stack |
main() |
- Kemudian
main()
akan dieksekusi, makafirstFunction()
akan dipanggil olehmain()
dan sebuah frame baru akan ditambahkan ke call stack.
Call Stack |
firstFunction() |
main() |
- Kemudian
console.log('First Function');
dieksekusi dan mencetak "First Function".
Call Stack |
firstFunction() --> console.log('First Function') |
main() |
- Kemudian
secondFunction()
dipanggil dari dalamfirstFunction()
, kemudian sebuah frame baru akan ditambahkan ke call stack di atasfirstFunction()
.
Call Stack |
secondFunction() |
firstFunction() |
main() |
- Kemudian
console.log('Second Function');
dieksekusi dan mencetak "Second Function".
Call Stack |
secondFunction() --> console.log('Second Function') |
firstFunction() |
main() |
- Kemudian
thirdFunction()
dipanggil dari dalamsecondFunction()
, sehingga sebuah frame baru ditambahkan ke call stack di atassecondFunction()
.
Call Stack |
thirdFunction() |
secondFunction() |
firstFunction() |
main() |
- Kemudian
console.log('Third Function');
dieksekusi dan mencetak "Third Function".
Call Stack |
thirdFunction() --> console.log('Third Function') |
secondFunction() |
firstFunction() |
main() |
- Setelah
thirdFunction()
selesai, ia dihapus dari call stack, dan eksekusi kembali kesecondFunction()
.
Call Stack |
secondFunction() |
firstFunction() |
main() |
- Kemudian
console.log('Second Function End');
dieksekusi dan mencetak "Second Function End".
Call Stack |
secondFunction() --> console.log('Second Function End') |
firstFunction() |
main() |
- Setelah
secondFunction()
selesai, ia dihapus dari call stack, dan eksekusi kembali kefirstFunction()
.
Call Stack |
firstFunction() |
main() |
- Kemudian
console.log('First Function End');
dieksekusi dan mencetak "First Function End".
Call Stack |
firstFunction() --> console.log('First Function End') |
main() |
- Setelah
firstFunction()
selesai, ia dihapus dari call stack.
Call Stack |
main() |
- Setelah semua fungsi dalam program selesai dieksekusi, Global Execution Context (
main()
) juga dihapus dari call stack, dan program dianggap selesai dijalankan. Pada titik ini, call stack kembali kosong.
Call Stack |
Output yang dihasilkan pada console:
First Function
Second Function
Third Function
Second Function End
First Function End
Apa yang Terjadi Pada Call Stack Jika Terjadi Error?
Ketika terjadi error pada salah satu fungsi, JavaScript akan menghentikan eksekusi nya dan mencari apakah ada mekanisme error handling (seperti try...catch
) yang bisa menangani error tersebut.
Jika error tidak ditangani, JavaScript akan memulai proses "unwinding the stack", di mana fungsi-fungsi yang sedang dieksekusi akan secara bertahap dikeluarkan dari call stack hingga menemukan tempat yang dapat menangani error atau hingga seluruh call stack kosong.
Misalnya, jika ada error yang terjadi di thirdFunction()
dan tidak ada mekanisme error handling, maka:
thirdFunction()
akan dihapus dari call stack.JavaScript akan mencoba melanjutkan di
secondFunction()
, tetapi karena error tidak tertangani,secondFunction()
juga akan dihapus dari call stack.Proses ini akan berlanjut hingga kembali ke
firstFunction()
dan akhirnya ke global context (main).Jika tidak ada penanganan error sama sekali, program akan berhenti dan menampilkan pesan error.
Contoh error handling sederhana dengan try...catch
:
function firstFunction() {
try {
console.log('First Function');
secondFunction();
console.log('First Function End');
} catch (error) {
console.error('Error caught in firstFunction:', error.message);
}
}
function secondFunction() {
console.log('Second Function');
thirdFunction();
console.log('Second Function End');
}
function thirdFunction() {
throw new Error('Something went wrong in thirdFunction');
}
firstFunction();
Output:
Pada contoh di atas, ketika thirdFunction()
memberikan suatu error, JavaScript akan mencari mekanisme catch
untuk menangani error tersebut. Karena firstFunction()
menggunakan try...catch
, error tersebut akan ditangkap dan di handle di sana, dan program tidak mengalami crash.
Apa yang Terjadi Jika Call Stack Terlalu Penuh?
Call stack memiliki kapasitas yang terbatas, dan jika sebuah program menumpuk terlalu banyak fungsi di dalam call stack tanpa pernah mengeluarkannya, call stack akan "penuh". Ketika ini terjadi, akan muncul sebuah error yang dikenal dengan nama stack overflow.
Stack overflow biasanya terjadi dalam kasus rekursif yang tidak terkendali atau fungsi yang terus menerus memanggil dirinya sendiri tanpa ada kondisi untuk berhenti. Sebagai contoh:
function recursiveFunction() {
recursiveFunction();
}
recursiveFunction();
Pada kode di atas, fungsi recursiveFunction()
akan terus memanggil dirinya sendiri tanpa henti, yang menyebabkan call stack terus bertambah hingga melampaui batas memori yang tersedia. Hasilnya, akan muncul error:
Uncaught RangeError: Maximum call stack size exceeded
Error ini berarti call stack sudah melebihi kapasitas maksimum yang bisa ditangani oleh JavaScript, sehingga eksekusi program dihentikan.
Heap
Heap dalam JavaScript adalah area memori untuk menyimpan objek dan data yang bersifat dinamis dan ukurannya bisa berubah, sehingga tidak cocok disimpan di call stack. Objek seperti array dan fungsi (yang juga dianggap objek di JavaScript) disimpan di heap.
Call stack digunakan untuk melacak eksekusi fungsi. Ketika sebuah fungsi dipanggil, referensinya (pointer) ditempatkan di call stack. Jika fungsi tersebut membuat objek atau variabel yang kompleks, data tersebut disimpan di heap, sementara referensinya tetap ada di call stack.
Pengelolaan heap dilakukan oleh garbage collector, yang secara otomatis membersihkan memori dari objek yang tidak lagi digunakan untuk memastikan efisiensi memori.
Mari kita lihat contoh berikut:
function hello() {
let name = 'john';
let age = 21;
let obj = {
first: 'left',
two: 'right'
};
let arr = [1, 2, 3];
function inner() {
console.log('hello ' + name);
}
return inner
}
let helloInner = hello();
helloInner();
Penjelasan:
Data tipe primitif seperti
name
danage
akan disimpan di stack karena ukurannya kecil dan bersifat statis (tidak dinamis).Data tipe referensi seperti
obj
danarr
adalah data yang bisa berubah ukurannya. Karena itu, data ini disimpan di heap memory, sedangkan di call stack hanya terdapat referensi atau pointer untuk mengaksesnya.Jika referensi ke data dinamis seperti
obj
danarr
hilang (misalnya, di-set menjadinull
), maka data yang tersimpan di heap akan dibersihkan oleh garbage collector.
Yang menarik di sini adalah ketika kita membuat fungsi lain bernama inner
di dalam fungsi hello
.
Kenapa?
Saat
inner
dibentuk sebagai closure, fungsi ini dapat mengakses variabelname
dari lingkup (scope) luar, yaitu dari fungsihello
.Meskipun variabel
name
adalah tipe primitif yang biasanya disimpan di stack, namun karena closure, referensi ke variabel ini dipindahkan ke heap untuk memastikan variabel tersebut tetap tersedia saatinner
nanti dipanggil.
Closure menyebabkan variabel yang di-capture seperti name
tetap tersimpan di heap karena fungsi inner
masih membutuhkan referensi tersebut. Jadi, meskipun eksekusi fungsi hello
telah selesai, variabel name
tidak akan dihapus dari memori sampai referensi ke fungsi inner
(seperti helloInner
) dihapus atau tidak digunakan lagi.
Apa yang terjadi jika heap terlalu penuh atau terlalu banyak alokasi memori tanpa me-release nya kembali?
Maka akan terjadi error "Out of Memory" atau "Allocation failed - JavaScript heap out of memory"
Web APIs
Ketika call stack mengeksekusi fungsi, JavaScript memeriksa apakah fungsi tersebut bersifat synchronous atau asynchronous. Jika fungsi bersifat asynchronous, seperti setTimeout
, setInterval
, atau fetch
, call stack akan mengirimkannya ke Web APIs untuk diproses di luar call stack. Web APIs ini berperan sebagai internal proses yang disediakan oleh browser agar bisa menjalankan tugas asynchronous tanpa memblokir eksekusi fungsi lainnya.
Fungsi asynchronous ini biasanya memerlukan waktu pemrosesan, seperti menunggu respons jaringan atau perlunya menunda eksekusi.
Setelah Web APIs menyelesaikan tugasnya, hasil atau callback dari fungsi asynchronous tersebut akan dikirim ke callback queue. Event loop kemudian akan mengirimkan callback dari callback queue ke call stack untuk dieksekusi.
Event Loop
Event Loop adalah mekanisme yang memungkinkan JavaScript menjalankan operasi non-blocking meskipun hanya berjalan di dalam single-thread. Event Loop bertanggung jawab untuk mengatur eksekusi kode, memproses event, dan menjalankan tugas-tugas asynchronous yang berada dalam antrian (queue).
Contoh sederhana untuk memahami cara kerja Event Loop:
console.log('Start');
setTimeout(() => {
console.log('Callback');
}, 1000);
console.log('End');
Cara kerja event loop:
console.log('Start')
dieksekusi dan mencetak "Start" di call stack.setTimeout
dipanggil dengan timer 1000ms dan dikirim ke Web APIs, sementara itu timer akan berjalan di luar call stack.console.log('End')
dieksekusi dan mencetak "End" di call stack.Setelah 1000ms, Web APIs akan mengirim callback dari
setTimeout
ke callback queue (antrian).Kemudian event loop memeriksa call stack apakah sedang kosong atau tidak.
Lalu di saat callback kosong, event loop akan mengambil sebuah fungsi callback dari antrian callback untuk di eksekusi.
Kemudian callback dari
setTimeout
dieksekusi di call stack, dan "Callback" dicetak.
Dari cara kerja di atas, kita bisa melihat bahwa event loop melakukan beberapa tugas seperti memeriksa antrian callback, mengambil fungsi callback yang akan dieksekusi, lalu mengirimkannya ke call stack. Dengan mekanisme ini, JavaScript mampu menangani operasi-operasi yang asinkron tanpa memblokir eksekusi kode lainnya.
Kemudian muncul pertanyaan.
Apa yang terjadi jika saat ada antrian callback namun call stack masih bekerja untuk operasi lain atau belum kosong?
Jika call stack masih sibuk, maka eksekusi fungsi yang ada di antrian callback akan tertunda, event loop akan terus memeriksa call stack secara berkala agar bisa segera memproses fungsi callback saat call stack kosong.
Sekarang mari kita lihat dengan contoh yang lebih nyata.
Contoh Kasus: Lazy Loading untuk UX yang Lebih Baik
Bayangkan Anda membuka sebuah situs galeri foto.
Misalnya itu seperti situs Unsplash atau Pixabay, namun dengan foto-foto yang memiliki resolusi tinggi.
Tanpa event loop:
Saat halaman dibuka, browser akan mengunduh semua gambar sekaligus
Sebelum unduhan selesai, halaman mungkin hanya menampilkan sebuah indikator loading
Pengguna tidak bisa berinteraksi dengan halaman website sampai semua unduhan selesai dan ditampilkan
Yang bisa dilihat hanyalah indikator loading, dan itu dalam waktu yang lama
Halaman tidak akan responsif selama proses ini
Dengan event loop:
Halaman akan dibuka dengan placeholder atau gambar resolusi rendah dulu, diwaktu yang sama proses download sudah dimulai dibelakang layar secara asinkronus.
Gambar-gambar akan muncul satu per satu saat proses pengunduhan selesai, hal ini memberikan progres visual yang dapat dilihat pengguna.
Pengguna bisa langsung berinteraksi, misalnya membaca caption, scrolling atau ber interaksi langsung dengan gambar asli yang sudah selesai di download tanpa mengganggu proses lain.
Jika koneksi internet lambat, pengguna masih bisa melihat progres secara langsung pada foto yang muncul satu per satu, hal ini akan menghindari rasa frustrasi karena halaman yang tampak tidak responsif.
Disini Event Loop Memiliki Peran Untuk:
Menjaga interaktivitas halaman tetap berjalan lancar
Memungkinkan tugas berat (unduhan gambar) berjalan di latar belakang
Tidak memblokir interaksi pengguna dengan halaman, sehingga pengguna tetap bisa berinteraksi dengan elemen lain di halaman.
Mengelola proses asinkron dengan memunculkan satu per satu unduhan yang sudah selesai, memberikan pengalaman pengguna yang lebih responsif dan dinamis.
Penutup
Dalam artikel ini, kita telah mempelajari tentang mekanisme Event Loop pada JavaScript, termasuk peran penting dari Call Stack, dan Web APIs dalam menjalankan operasi asynchronous. Kita juga telah melihat sebuah contoh bagaimana Event Loop dapat menjaga interaktivitas halaman website tetap lancar dan responsif.
Namun, kita belum membahas lebih detail tentang callback queue, yang dibagi menjadi dua yaitu task queue dan microtask queue.
Apa-apa saja perbedaannya? dan bagaimana mereka dapat mempengaruhi event loop?
Kita akan coba eksplorasi lebih lanjut di artikel berikutnya!