JAVASCRIPT EVENT LOOP NEDİR?

JavaScript event loop merkezi döngü hub'ı ve call stack ile task queue bağlantısı diyagramı

Buton tıklanıyor, ekran kilitleniyor. Kullanıcı bir kez daha basıyor, hâlâ tepki yok. Konsolda hata da yok. Sorun şu: arka planda 4 saniye süren bir hesaplama döngüsü çalışıyor ve tarayıcı bu süre boyunca tek bir tıklamayı dahi işleyemiyor. JavaScript'in tek thread'li yapısı bu kilitlenmenin sebebi; event loop ise neden böyle olduğunun ve nasıl aşılacağının cevabı.

Tek Thread Bir Dil, Eşzamanlı Görünen Bir Dünya

JavaScript aynı anda yalnızca tek bir iş yapar. Yani for döngüsü çalışırken başka hiçbir kod, tıklama olayı veya zamanlayıcı geri çağrısı yürütülemez. Buna rağmen bir web sayfasında onlarca şey eşzamanlı oluyormuş gibi görünür: animasyon devam eder, fetch isteği yanıt bekler, kullanıcı yazı yazar. Bu yanılsamanın arkasındaki mekanizma event loop'tur.

Event loop tek başına çalışmaz. Call stack, web API'leri (veya Node tarafında C++ API'leri), task queue ve microtask queue ile birlikte bir orkestra gibi davranır. Her birinin görevi farklıdır.

Call Stack: Sıradaki İş Burada Çalışır

Call stack, çalışmakta olan fonksiyonların LIFO mantığıyla tutulduğu yığındır. Bir fonksiyon çağrıldığında stack'e push edilir, return ettiğinde pop edilir. JavaScript motoru her an yalnızca stack'in en üstündeki fonksiyonu yürütür.

  • Stack boş değilse event loop hiçbir şey yapamaz; sıradaki callback'ler beklemek zorundadır.
  • Stack boşaldığında event loop devreye girer ve kuyruklardan yeni bir iş çeker.
  • Stack overflow, sonsuz rekürsiyonun stack'i taşırması durumudur.

Buradan çıkan kritik sonuç: stack'te uzun süren senkron bir iş varsa, tarayıcı diğer her şeyi (render, click, scroll) bekletmek zorunda kalır.

Long-Running Sync Function UI'yi Neden Dondurur

Şu örneği düşünün:

button.addEventListener("click", () => { let sum = 0; for (let i = 0; i < 1e10; i++) sum += i; });

Tıklamadan sonra döngü tamamlanana kadar call stack bu callback ile dolu kalır. Bu sırada:

  1. Tarayıcı yeni tıklamaları algılar ama callback'lerini çalıştıramaz; kuyruğa atar.
  2. Repaint adımı atlanır — animasyonlar donar.
  3. Kullanıcı "sayfa yanıt vermiyor" uyarısıyla karşılaşır.

Çözüm callback'i parçalamak veya işi web worker gibi farklı bir thread'e devretmektir. Asenkron API'lerin var olma nedeni tam olarak budur.

Call stack ve task queue arasında event loop'un callback aktarımını gösteren akış diyagramı

Web API'leri ve Task Queue

setTimeout, fetch, DOM event listener gibi işler aslında JavaScript motorunun değil, tarayıcının (veya Node ortamının) sorumluluğundadır. Bir setTimeout(fn, 1000) çağrıldığında:

  • Timer tarayıcının web API katmanına teslim edilir.
  • JavaScript motoru hemen bir sonraki satıra geçer; bekleme yapmaz.
  • Süre dolduğunda callback, task queue'ya (macrotask queue) eklenir.
  • Event loop, call stack boşaldığında bu callback'i alır ve stack'e push eder.

Bu yüzden setTimeout(fn, 0) "hemen çalıştır" demek değildir; "stack boşalır boşalmaz, ondan sonra çalıştır" demektir.

Microtask Queue: Promise'ler Burada Yaşar

Microtask queue, task queue'dan farklı ve ondan önceliklidir. Promise.then, queueMicrotask, MutationObserver callback'leri buraya düşer. Event loop'un altın kuralı:

Bir task bittiğinde, sıradaki task'a geçmeden önce microtask queue tamamen boşaltılır. Bu, peş peşe zincirlenmiş promise'lerin neden render adımları arasına sıkışmadan çalıştığını açıklar. Kuyruk önceliklerinin ve task sınıflandırmasının formal tanımları için resmi dokümantasyonu incelemek faydalı olur.

Pratik sonuç: microtask queue'da kendini sürekli besleyen bir zincir oluşturursanız (her .then yenisini eklerse) tarayıcı asla render edemez. Buna "microtask starvation" denir.

Render Adımı Nereye Sığıyor?

Tarayıcı her task arasında potansiyel olarak bir render adımı çalıştırır: style hesabı, layout, paint, composite. Ancak render zorunlu değildir — tarayıcı genelde saniyede 60 kez fırsat kollar. Eğer task'lar arası boşluk yoksa render adımı atlanır ve sayfa "donmuş" gibi görünür.

requestAnimationFrame, callback'i bir sonraki render'dan hemen önce çalıştırır; bu yüzden animasyonlar için setTimeout yerine tercih edilir.

Microtask ve macrotask kuyrukları ile render adımının event loop önceliğine göre sıralanması

Pratikte Ne Yapmalı?

  • Ağır hesaplamayı böl: requestIdleCallback veya setTimeout ile parçala, her iterasyon sonunda kontrolü event loop'a iade et.
  • Web Worker kullan: Saf hesaplama yükünü ayrı thread'e taşı; ana thread render için boş kalsın.
  • Promise zincirinden kaçma: Microtask starvation riskini fark et, gerektiğinde setTimeout(fn, 0) ile macrotask sınırına geç.
  • DevTools Performance sekmesi: Long task'ları (50ms+) görmek için kullan; hangi fonksiyonun stack'i tıkadığını bulursun.

Asenkron JavaScript'in tüm mekaniğini ve callback / promise / async-await arasındaki ayrımı pekiştirmek için JavaScript eğitimi içeriğinden yararlanabilirsiniz; event loop bilgisi, bu konuların hepsinin altında yatan temeldir.

Node.js Tarafında Küçük Bir Not

Node.js, libuv üzerine kurulu farklı bir event loop modeline sahiptir: timers, pending callbacks, poll, check, close gibi fazlardan oluşur. setImmediate ile setTimeout(fn, 0) arasındaki ince farklar bu fazlardan kaynaklanır. Tarayıcı modelini bilen bir geliştirici Node tarafına geçtiğinde benzer mantıkla ama farklı isimlendirmelerle karşılaşır; ana fikir aynı kalır: stack boşalmadan callback çalışmaz.

Event loop kavramını içselleştirmek, "neden bu fetch sonucu beklenenden geç geldi", "neden bu animasyon takıldı", "neden async/await sırası bu" gibi sorulara doğrudan yanıt verir. Tek thread'li bir dilin nasıl olup da bu kadar çok şeyi aynı anda yapıyormuş gibi göründüğünün cevabı, küçük ama disiplinli bir döngüde gizli.