Tek thread'li ama sıralı değil
JavaScript tek bir thread üzerinde çalışır. Tek bir call stack vardır ve herhangi bir anda tam olarak bir fonksiyon yürütülür. Aynı realm içinde kodunuzun iki satırı asla paralel çalışmaz.
Kulağa kısıtlayıcı geliyor, ta ki JavaScript'in aslında ne yaptığını hatırlayana kadar: veri çekmek, kullanıcının tıklamasını beklemek, dosya okumak. "İş"in büyük kısmı aslında beklemek. İşte event loop, beklemeyi ucuza getiren o numara — kodunuz bir görevi tarayıcıya ya da Node'a devreder, kendi işine döner ve sonuç hazır olduğunda haber alır.
birinci ve ikinci sırasıyla yazdırılır. üçüncü ise — timeout 0 olmasına rağmen — en sonda yazdırılır. Arada oluşan bu boşluğun sebebi event loop'tur ve bu sayfanın asıl amacı da bunun neden böyle olduğunu anlamak.
Call Stack (Çağrı Yığını)
Her fonksiyon çağrısı, call stack'e bir frame ekler. Fonksiyon return ettiğinde ise bu frame yığından çıkar. Stack dediğimiz şey sonuçta bir yığın: son giren ilk çıkar (LIFO).
outer() çağrıldığında Node önce outer'ı stack'e atar, sonra inner'ı; inner "done" döndürdüğünde onu pop eder, ardından outer'ı da pop eder. Stack yine boş. İşte event loop'un kolladığı an tam olarak bu "stack boş" anıdır.
Senkron kod, stack üzerinde baştan sona kesintisiz çalışır. Hiçbir async iş onu bölemez. while (true) gibi bir döngü yazarsan stack hiç boşalmaz ve sayfa kilitlenir — tıklamalar, timer'lar, promise callback'leri, hiçbiri çalışmaz. Event loop'a sıra hiç gelmediği için yapacak bir şeyi de olmaz.
Async işler aslında nerede çalışıyor?
JavaScript'in kendisi aslında ne bir network isteği atmayı bilir, ne de 100 milisaniye beklemeyi. Bu API'ler host ortamına aittir — yani tarayıcı ya da Node. setTimeout(fn, 100) çağırdığında şöyle bir şey oluyor:
- Timer host tarafında kaydedilir.
setTimeoutanında geri döner. Stack kaldığı yerden devam eder.- 100ms sonra host,
fn'yi bir kuyruğa (queue) bırakır. - Stack boşaldığında event loop,
fn'yi kuyruktan alıp çalıştırır.
Zamanlayıcının geri çağırma fonksiyonu, for döngüsü ve console.log("son") bitmeden çalışamaz — çünkü call stack henüz boşalmış değil. Zamanlayıcılar en az şu kadar beklenecek anlamına gelir; garanti edilen bir süre değildir.
İki Kuyruk: Task ve Microtask
JavaScript'te tek bir kuyruk yok, iki tane var. Event loop ile ilgili kafa karıştırıcı durumların çoğu da bu ayrımdan kaynaklanıyor.
- Task kuyruğu (bazen macrotask kuyruğu olarak da geçer):
setTimeout,setInterval, I/O callback'leri, UI olayları. - Microtask kuyruğu: promise callback'leri (
.then,.catch,.finally),awaitsonrası devam eden kod vequeueMicrotaskile planlanan her şey.
Event loop'un izlediği kural şu:
- Task kuyruğundan bir task al ve çalıştır.
- Microtask kuyruğunu tamamen boşalt — boşaltma sırasında yeni eklenenler dahil, hepsi çalışsın.
- Gerekirse render et (tarayıcılarda).
-
- adıma dön.
Yani microtask'lar her zaman bir sonraki task'tan önce çalışır. İşte tam da bu yüzden aşağıdaki örnek çoğu kişiyi şaşırtır:
Çıktının sırası şöyle: sync 1, sync 2, promise, timeout. Önce senkron kod çalışır. Ardından call stack boşalır. Sonra event loop microtask'ları boşaltır (promise). Ve ancak bundan sonra timer task'ını alır (timeout).
Microtask'lar Task'ları Aç Bırakabilir
Microtask queue, bir sonraki task'a geçilmeden önce tamamen boşaltıldığı için, sürekli yeni microtask planlayan bir microtask task queue'yu sonsuza kadar bloklar:
Timer asla tetiklenmez; çünkü her microtask bir başka microtask ekler ve event loop kuyruğun boşalmasına fırsat bulamaz. Promise zincirleri bu açıdan güvenlidir — her .then yalnızca tek bir devam (continuation) planlar. Ama elle yazılmış microtask döngüleri, bilmekte fayda olan ciddi bir ayak kurşunudur.
await Aslında Bir Microtask İçin Söz Dizimi Şekeri
Bir promise'i await ettiğinde fonksiyon duraklar ve geri kalan kısmı, promise sonuçlandığında çalışmak üzere microtask olarak kuyruğa alınır. Sihirli bir şey yok — arka planda hâlâ .then çalışıyor.
Çıktı: A, C, B. await kontrolü çağırana geri veriyor. console.log("C") mevcut stack üzerinde çalışır. Ardından microtask queue boşalır ve demo kaldığı yerden devam ederek B yazdırır.
Async kod okurken bunu aklında tut: await bloklamaz — sırayı devreder.
Her Şeyi Sıraya Dizen Bir Örnek
Şimdi tüm parçaları bir araya getirelim:
Sıra şöyle işliyor:
1: sync— stack üzerinde çalışır.6: sync— hâlâ stack üzerinde.- Stack boşalır. Microtask queue boşaltılır: önce
3: promise, sonra5: microtask, ardından4: nested microtask(boşaltma sırasında kuyruğa eklenmiş olmasına rağmen yine de işlenir). - Sıradaki task çalışır:
2: timeout.
Nihai çıktı: 1, 6, 3, 5, 4, 2. Bu akışı kafanda takip edebiliyorsan, event loop'u kavramışsın demektir.
Node'da Daha Fazla Faz Var
Node'un event loop'u, tarayıcıdaki modelin genişletilmiş bir versiyonudur. Birbirinden ayrı fazları vardır — timers, pending I/O callbacks, poll, check, close — ve microtask'lar her fazın arasında boşaltılır. setImmediate check fazında çalışır; process.nextTick ise normal microtask'lardan önce çalışır (kendine ait, daha da yüksek öncelikli bir kuyruğu vardır).
İlk günden faz tablosunu ezberlemene gerek yok. Asıl kritik nokta, tarayıcıdakiyle aynı: önce senkron kod baştan sona çalışır, sonra microtask'lar boşaltılır, ardından loop kuyruğa alınmış bir sonraki callback'i ele alır.
Neden Önemli?
Bu model kafanda oturduğunda, pek çok async davranışın gizemi kalkıyor:
- Uzun bir
fordöngüsü UI'ını donduruyorsa sebebi basit: event loop sıra bulamıyor. setTimeout(fn, 0), bir işi mevcut task ve microtask'lar bittikten sonraya ertelemenin yoludur.- Zaten resolve olmuş bir promise'in
.thencallback'i "anında" çalışıyormuş gibi görünse de, mevcut senkron kod bitmeden devreye girmez. - Döngü içindeki
await, her iterasyon devam etmeden önce microtask queue'ya sıra verdiği için işleri sıralı hâle getirir.
Async kod debug etmek, büyük ölçüde şu soruyu sormaktan ibarettir: "Şu an stack'te ne var, kuyrukta ne bekliyor?" Cevap her zaman event loop'ta gizli.
Sırada: Callback'ler
Promise'ler ve async/await gelmeden önce JavaScript'in async iş için tek aracı callback'lerdi — bir API'ye verdiğin, onun da daha sonra çağıracağı bir fonksiyon. Callback'ler hâlâ her yerde karşımıza çıkıyor (event listener'lar, Node'un temel API'leri) ve bu bölümde işleyeceğimiz her şeyin temelini onları anlamak oluşturuyor.
Sıkça Sorulan Sorular
JavaScript'te event loop nedir?
Tek thread üzerinde çalışan JavaScript'in async işleri bloklamadan yürütmesini sağlayan mekanizmadır. Event loop sürekli call stack'i izler; stack boşaldığı an sıradaki callback'i kuyruktan çekip çalıştırır. Timer'lar, I/O işlemleri ve promise devamları hep kuyruklara düşer, event loop da bunları tek tek işler.
JavaScript neden tek thread'li çalışır?
Dil spesifikasyonu her realm için tek bir call stack tanımlar, yani kodunuz tek bir thread üzerinde koşar. Eşzamanlılık aslında host tarafından (tarayıcı ya da Node) geliyor: timer, network, dosya I/O gibi işler arka plandaki API'lere devrediliyor, iş bitince de bir callback kuyruğa atılıyor. Aynı context içinde aynı anda iki parça JS çalıştığını asla göremezsiniz.
Microtask ile macrotask arasındaki fark nedir?
Microtask'lar promise'lerden (.then, await) ve queueMicrotask'tan gelir. Macrotask'lar ise setTimeout, setInterval, I/O ve UI olaylarından. Event loop her macrotask bittikten sonra bir sonraki macrotask'a geçmeden önce microtask kuyruğunun tamamını boşaltır. Bu yüzden aynı anda planlanmış bir Promise.resolve().then(...) her zaman setTimeout(..., 0)'dan önce çalışır.
setTimeout 0ms ile neden hemen çalışmaz?
setTimeout(fn, 0) "şimdi çalıştır" demek değildir; "fn'i macrotask olarak kuyruğa al, en erken 0ms sonra çalışsın" demektir. Önce o an çalışan senkron kodun bitmesi, sonra microtask kuyruğunun boşalması gerekir; event loop timer callback'inizi ancak bundan sonra alır. Yani 0 bir garanti değil, bir alt sınırdır.