Model satu thread dari topik Cara kerja JavaScript meninggalkan satu pertanyaan besar, yaitu bagaimana JavaScript tetap sanggup menunggu timer, menampung klik, dan mengambil data jaringan tanpa membekukan program. Jawabannya ada pada event loop, mekanisme yang mengatur giliran pekerjaan di sekitar satu call stack. Topik ini merinci komponen-komponennya, membuktikan urutan eksekusi baris sinkron, callback timer, dan callback then pada Node.js, lalu menunjukkan akibatnya ketika kode berurutan memakan waktu lama. Setelah membacanya, urutan keluaran program yang memadukan berbagai jenis callback dapat diprediksi tanpa menebak.
Call stack dan satu jalur eksekusi#
Call stack adalah struktur pencatat pemanggilan fungsi yang sedang berjalan, dan satu tumpukan itulah seluruh dunia eksekusi JavaScript. Setiap fungsi yang dipanggil menumpuk catatannya di paling atas, dan catatan itu diangkat ketika fungsinya selesai. Karena tumpukannya hanya satu, dua pekerjaan tidak pernah berjalan serentak. Pekerjaan kedua baru dimulai setelah pekerjaan pertama benar-benar selesai. Sifat inilah yang menyebabkan kode berjalan secara sinkron, urut dari atas ke bawah tanpa melompat.
Program berikut menaruh callback timer di antara baris-baris sinkron. Berdasarkan model tumpukan, seluruh baris sinkron dieksekusi lebih dahulu, sedangkan callback setTimeout belum bisa disentuh karena call stack belum kosong.
// Membandingkan urutan kode sinkron dan callback setTimeout.
console.log("1: baris sinkron pertama");
setTimeout(() => {
console.log("4: callback setTimeout 0 ms");
}, 0);
console.log("2: baris sinkron kedua");
console.log("3: baris sinkron ketiga");1: baris sinkron pertama
2: baris sinkron kedua
3: baris sinkron ketiga
4: callback setTimeout 0 msKeluarannya menegaskan dua hal. Baris sinkron selalu beres lebih dahulu karena semuanya dieksekusi langsung dari call stack. Callback timer tidak diperlakukan sebagai baris program, melainkan sebagai pekerjaan yang dijadwalkan, dan jadwalnya baru dilayani setelah call stack bersih. Jeda yang ditulis nol milidetik pun tidak mengubah urutan itu, karena menunggu giliran bukan menunggu waktu.
Task queue dan kerja event loop#
Pertanyaannya tinggal satu, di mana callback yang sudah dijadwalkan itu menunggu giliran. Jawabannya adalah task queue, yakni antrean tugas yang berisi callback yang sudah siap dieksekusi. Ketika setTimeout dipasang, pengatur waktunya bukan thread JavaScript, melainkan lingkungan di sekitarnya, yaitu browser atau Node.js. Begitu waktunya habis, lingkungan tersebut menaruh callback ke task queue, dan dari titik ini event loop mengambil alih.
Event loop bekerja seperti penjaga yang terus memeriksa dua tempat. Selama call stack masih berisi, antrean tidak disentuh sama sekali. Begitu call stack kosong, tugas paling depan di antrean diangkat lalu dipasang di call stack untuk dieksekusi. Siklus pemeriksaan ini berulang sepanjang program hidup, sehingga seluruh pekerjaan asinkron terlihat bergantian rapi padahal semuanya dilayani oleh satu tumpukan yang sama.
Diagram berikut merangkum alur satu tugas timer, dari pemasangan jadwal sampai callback-nya akhirnya dieksekusi.
Mikrotask didahulukan sebelum makrotask#
Antrean tugas yang dibahas di atas bukan satu-satunya antrean. Callback yang dipasang lewat then pada Promise.resolve() masuk ke antrean tersendiri yang disebut mikrotask, sedangkan callback setTimeout masuk antrean makrotask. Bedanya terletak pada waktu pelayanan. Setelah call stack kosong, seluruh mikrotask dilayani sampai tuntas lebih dahulu, baru kemudian satu makrotask berikutnya diambil. Program berikut menaruh keduanya berdampingan agar bedanya terlihat langsung.
// Mikrotask berjalan sebelum makrotask berikutnya.
console.log("1: baris sinkron");
setTimeout(() => {
console.log("4: makrotask dari setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("3: mikrotask dari then");
});
console.log("2: baris sinkron terakhir");1: baris sinkron
2: baris sinkron terakhir
3: mikrotask dari then
4: makrotask dari setTimeoutUrutannya selalu sama, baris sinkron terlebih dahulu, kemudian mikrotask, kemudian makrotask. Mikrotask didahulukan karena pekerjaannya biasanya lanjutan langsung dari operasi yang baru selesai, misalnya mengolah hasil yang sudah tersedia, sehingga menundanya di belakang timer justru merugikan. Pembagian ini sekaligus menjelaskan bahwa callback then tidak pernah berjalan di tengah kode sinkron, ia tetap menunggu call stack kosong, hanya saja gilirannya datang lebih awal daripada timer.
Peragaan berikut mengulang urutan yang sama di dalam browser. Tekan tombolnya lalu bandingkan urutan barisnya dengan keluaran program Node di atas.
Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Demo urutan event loop</title>
<style>
body { font-family: "Segoe UI", Arial, sans-serif; margin: 14px; font-size: 14px; color: #1f2937; }
button { padding: 6px 12px; margin-right: 8px; margin-bottom: 10px; cursor: pointer; }
pre { font-size: 13px; line-height: 1.7; background: #f3f4f6; padding: 10px; min-height: 110px; }
</style>
</head>
<body>
<p>Tekan tombolnya dan perhatikan urutan baris yang muncul.</p>
<button id="tombol-jalankan" type="button">Jalankan urutan</button>
<pre id="log"></pre>
<script src="index.js"></script>
<script>
// Demo event loop, urutan baris sinkron, mikrotask, dan makrotask di browser.
const log = document.getElementById("log");
function catat(pesan) {
log.textContent = log.textContent + pesan + "\n";
}
document.getElementById("tombol-jalankan").addEventListener("click", () => {
log.textContent = "";
catat("1: baris sinkron");
setTimeout(() => {
catat("4: makrotask dari setTimeout");
}, 0);
Promise.resolve().then(() => {
catat("3: mikrotask dari then");
});
catat("2: baris sinkron terakhir");
});
</script>
<script>
// Demo event loop, urutan baris sinkron, mikrotask, dan makrotask di browser.
const log = document.getElementById("log");
function catat(pesan) {
log.textContent = log.textContent + pesan + "\n";
}
document.getElementById("tombol-jalankan").addEventListener("click", () => {
log.textContent = "";
catat("1: baris sinkron");
setTimeout(() => {
catat("4: makrotask dari setTimeout");
}, 0);
Promise.resolve().then(() => {
catat("3: mikrotask dari then");
});
catat("2: baris sinkron terakhir");
});
</script></body>
</html>Kode berat menunda semua antrean#
Semua antrean tadi dilayani hanya ketika call stack kosong, sehingga satu pekerjaan sinkron yang berat menunda semuanya sekaligus. Program berikut memasang timer dengan jeda 10 ms, lalu sengaja menghabiskan sekitar 100 ms untuk pekerjaan sinkron berupa perulangan kosong.
// Pekerjaan sinkron yang berat menunda callback timer.
console.log("mulai");
setTimeout(() => {
console.log("callback timer 10 ms, baru jalan sekarang");
}, 10);
const mulai = Date.now();
while (Date.now() - mulai < 100) {
// sengaja memblokir sekitar 100 ms
}
console.log("loop berat selesai");mulai
loop berat selesai
callback timer 10 ms, baru jalan sekarangTimernya dijadwalkan lebih dulu, tetapi callback-nya baru tampil setelah loop selesai. Selama sekitar 100 ms itu call stack tidak pernah kosong, sehingga event loop tidak mendapat kesempatan mengangkat tugas apa pun dari antrean. Di dalam Node.js, akibatnya berupa callback yang datang terlambat, sedangkan di browser efeknya jauh lebih terasa karena gambar, klik, dan ketikan dilayani oleh jalur yang sama.
Ringkasan#
- Call stack adalah satu-satunya tempat eksekusi JavaScript, sehingga pekerjaan selalu diproses satu per satu.
- Callback yang dijadwalkan menunggu di task queue dan baru dieksekusi ketika event loop menemukan call stack kosong.
- Mikrotask dilayani sampai tuntas sebelum satu makrotask berikutnya, sehingga callback
thenmendahului callbacksetTimeout. - Kode sinkron yang berat menunda seluruh antrean, dan di browser akibatnya berupa antarmuka yang membeku.
Referensi#
- Concurrency model and the event loop — MDN Web Docs — penjelasan resmi model konkurensi, call stack, dan antrean tugas.
- Event loop — The Modern JavaScript Tutorial — bab yang merinci mikrotask, makrotask, beserta peragaan urutannya.
- The Node.js Event Loop, Timers, and process.nextTick() — Node.js — panduan event loop khusus lingkungan Node.js.