Single-Threaded, Tetapi Tidak Berurutan
JavaScript berjalan di satu thread. Ada satu call stack, dan pada satu waktu tepat satu fungsi yang sedang dieksekusi. Tidak pernah ada dua baris kodemu yang berjalan paralel di dalam realm yang sama.
Kedengarannya membatasi, sampai kamu ingat apa yang sebenarnya dilakukan JavaScript: mengambil data, menunggu klik pengguna, membaca file. Sebagian besar "pekerjaannya" adalah menunggu. Event loop adalah trik yang membuat menunggu jadi murah: kodemu menyerahkan tugas ke browser atau Node, kembali mengerjakan hal lain, lalu diberi tahu saat hasilnya siap.
first dan second tercetak berurutan. third tercetak setelahnya, padahal timeout-nya 0. Jeda itu adalah hasil kerja event loop, dan memahami alasannya adalah inti dari halaman ini.
Call Stack
Setiap pemanggilan fungsi mendorong sebuah frame ke call stack. Saat fungsi itu selesai, frame-nya dikeluarkan. Stack hanyalah tumpukan: yang terakhir masuk, yang pertama keluar.
Saat outer() berjalan, Node mendorong outer, lalu inner, lalu mengeluarkan inner saat ia mengembalikan "done", lalu mengeluarkan outer. Stack kosong lagi. Momen "stack kosong" itulah yang diawasi event loop.
Kode synchronous berjalan dari awal sampai akhir di stack. Tidak ada kode async yang bisa menyelanya. Kalau kamu menulis loop while (true), stack tidak pernah kosong dan halamannya membeku: tidak ada klik, tidak ada timer, tidak ada callback promise. Event loop tidak bisa berbuat apa-apa karena tidak pernah mendapat giliran.
Tempat Pekerjaan Async Sebenarnya Berada
JavaScript sendiri tidak tahu cara membuat request jaringan atau menunggu 100 milidetik. API-API itu milik host, yaitu browser atau Node. Saat kamu memanggil setTimeout(fn, 100), inilah yang terjadi:
- Timer didaftarkan ke host.
setTimeoutlangsung selesai. Stack terus berjalan.- 100ms kemudian, host menaruh
fndi sebuah antrean. - Saat stack kosong, event loop mengambil
fndari antrean dan menjalankannya.
Callback timer tidak bisa berjalan sampai loop for dan console.log("end") selesai, karena stack belum kosong. Timer adalah jeda minimum, bukan jaminan.
Dua Antrean: Task dan Microtask
Antreannya bukan cuma satu. Ada dua, dan perbedaannya menjelaskan sebagian besar kejutan seputar event loop.
- Task queue (kadang disebut macrotask queue):
setTimeout,setInterval, callback I/O, event UI. - Microtask queue: callback promise (
.then,.catch,.finally), kelanjutan setelahawait, dan apa pun yang dijadwalkan denganqueueMicrotask.
Aturan yang diikuti event loop:
- Jalankan satu task dari task queue.
- Kosongkan seluruh microtask queue, yaitu setiap microtask, termasuk yang dijadwalkan selama proses pengosongan.
- Lakukan render kalau perlu (di browser).
- Kembali ke langkah 1.
Microtask selalu berjalan sebelum task berikutnya. Itulah sebabnya contoh ini mengejutkan orang:
Urutan output-nya: sync 1, sync 2, promise, timeout. Kode synchronous berjalan lebih dulu. Lalu stack kosong. Lalu event loop mengosongkan microtask (promise). Baru setelah itu ia mengambil task timer (timeout).
Microtask Bisa Membuat Task Kelaparan
Karena microtask queue dikosongkan sepenuhnya sebelum task berikutnya, microtask yang terus menjadwalkan microtask lain akan memblokir task queue selamanya:
Timer itu tidak akan pernah berbunyi, karena setiap microtask mengantrekan microtask lain, dan loop-nya tidak pernah membiarkan antreannya kosong. Rangkaian promise aman karena setiap .then hanya menjadwalkan satu kelanjutan, tetapi loop microtask yang ditulis manual adalah jebakan yang perlu kamu ketahui.
await Adalah Sintaks Pemanis untuk Microtask
Saat kamu await sebuah promise, fungsinya berhenti sejenak dan sisa fungsinya dijadwalkan sebagai microtask yang berjalan setelah promise-nya selesai. Tidak ada sihir, hanya .then di baliknya.
Output-nya: A, C, B. await menyerahkan kendali kembali ke pemanggil. console.log("C") berjalan di stack saat ini. Lalu microtask queue dikosongkan dan sisa demo dilanjutkan, mencetak B.
Ingat ini saat membaca kode async. await tidak memblokir, melainkan memberi giliran.
Contoh Lengkap: Mengurutkan Semuanya
Gabungkan semua bagiannya:
Urutannya:
1: sync: berjalan di stack.6: sync: masih di stack.- Stack kosong. Microtask queue dikosongkan:
3: promise,5: microtask, lalu4: nested microtask(dijadwalkan selama pengosongan, tetap ikut diambil). - Task berikutnya berjalan:
2: timeout.
Output akhirnya: 1, 6, 3, 5, 4, 2. Kalau kamu bisa menelusuri contoh ini, kamu sudah memahami event loop.
Node Punya Lebih Banyak Fase
Event loop Node adalah perluasan dari model browser. Ia punya fase yang berbeda-beda, yaitu timers, pending I/O callbacks, poll, check, dan close, dan microtask dikosongkan di antara setiap fase. setImmediate berjalan di fase check, sedangkan process.nextTick berjalan sebelum microtask biasa (ia punya antrean sendiri dengan prioritas yang lebih tinggi lagi).
Kamu tidak perlu menghafal diagram fasenya di hari pertama. Pelajaran utamanya sama seperti di browser: kode synchronous berjalan sampai selesai, lalu microtask dikosongkan, lalu loop mengambil callback berikutnya dari antrean.
Mengapa Ini Penting
Begitu model ini kamu pahami, banyak kode async tidak lagi terasa misterius:
- Loop
foryang panjang membekukan UI-mu karena event loop tidak bisa mendapat giliran. setTimeout(fn, 0)adalah cara menunda pekerjaan sampai task dan microtask saat ini selesai.- Callback
.thenyang berjalan "langsung" setelah promise yang sudah selesai tetap menunggu kode synchronous saat ini berakhir. awaitdi dalam loop membuat pekerjaan berjalan satu per satu karena setiap iterasi memberi giliran ke microtask queue sebelum melanjutkan.
Debug kode async sebagian besar soal bertanya "apa yang ada di stack sekarang, dan apa yang sedang mengantre?" Event loop adalah jawabannya.
Berikutnya: Callback
Sebelum ada promise dan async/await, satu-satunya alat JavaScript untuk pekerjaan async adalah callback, yaitu fungsi yang kamu serahkan ke sebuah API untuk dipanggil nanti. Callback masih ada di mana-mana (event listener, API inti Node), dan memahaminya adalah fondasi untuk semua materi lain di bab ini.
Pertanyaan yang sering diajukan
Apa itu event loop di JavaScript?
Event loop adalah mekanisme yang memungkinkan JavaScript yang single-threaded menjalankan pekerjaan async tanpa memblokir. Loop ini mengawasi call stack: saat stack kosong, ia mengambil callback berikutnya dari antrean dan menjalankannya. Timer, I/O, dan kelanjutan promise semuanya berakhir di antrean yang dikosongkan event loop satu item demi satu item.
Mengapa JavaScript single-threaded?
Spesifikasi bahasanya menetapkan satu call stack per realm, jadi kodemu berjalan di satu thread. Konkurensi datang dari host (browser atau Node) yang menyerahkan pekerjaan ke API latar belakang, seperti timer, jaringan, dan I/O file, lalu mengantrekan callback saat pekerjaannya selesai. Kamu tidak akan pernah melihat dua potong JS berjalan bersamaan dalam konteks yang sama.
Apa bedanya microtask dan macrotask?
Microtask berasal dari promise (.then, await) dan queueMicrotask. Macrotask berasal dari setTimeout, setInterval, I/O, dan event UI. Setiap kali satu macrotask selesai, event loop mengosongkan seluruh microtask queue sebelum menjalankan macrotask berikutnya. Itulah sebabnya Promise.resolve().then(...) selalu berjalan sebelum setTimeout(..., 0) yang dijadwalkan pada saat yang sama.
Mengapa setTimeout dengan 0ms tidak langsung berjalan?
setTimeout(fn, 0) tidak berarti 'jalankan sekarang', melainkan 'antrekan fn sebagai macrotask, paling cepat setelah 0ms.' Kode synchronous yang sedang berjalan harus selesai, microtask queue harus kosong, dan baru setelah itu event loop mengambil callback timer-mu. Jadi 0 adalah batas bawah, bukan janji.