Müzakirə

JavaScript Event Loop Detaylı Anlatım

Başladan DarkProtocol · 10 iyl 2026 20:01 · 18 Baxış · 0 Cavablar
Mövzunu Açan #0
JavaScript Event Loop Detaylı Anlatım

Merhaba arkadaşlar,

Çoğumuz JavaScript ile çalışırken, özellikle asenkron işlemlerle uğraşırken bazı garip veya beklenmedik davranışlarla karşılaşmışızdır. Belki bir
CODE
1setTimeout(fn, 0)
kullandığımızda kodumuzun hemen değil, biraz sonra çalıştığını görmüşüzdür. Ya da aynı anda birden fazla uzun süren işlem başlattığımızda, tarayıcının donduğunu veya Node.js uygulamasının yanıt vermeyi kestiğini fark etmişizdir. İşte tüm bu gizemlerin arkasında yatan, JavaScript'in kalbi diyebileceğimiz bir mekanizma var: Event Loop, yani Olay Döngüsü. Bugün bu konuyu enine boyuna, tüm detaylarıyla ele alalım istedim.

JavaScript'in tek iş parçacıklı (single-threaded) bir dil olduğunu hepimiz biliyoruz. Peki bu ne anlama geliyor? Basitçe söylemek gerekirse, JavaScript motoru (V8 gibi), aynı anda sadece tek bir görevi yürütebilir. Yani bir kod bloğu çalışırken, başka hiçbir şey yapılamaz; DOM güncellenemez, kullanıcı etkileşimleri yanıtlanamaz, ağ istekleri işlenemez. Bu durum, senkron kod yazarken pek sorun teşkil etmez. Ancak modern web uygulamalarında veya Node.js backend'lerinde, ağ istekleri, dosya okuma/yazma, zamanlayıcılar gibi uzun sürebilecek işlemlerle sıkça karşılaşıyoruz. Eğer bu işlemler ana iş parçacığını bloke etseydi, uygulamalarımız tamamen donuk ve kullanılamaz hale gelirdi. İşte burada Event Loop devreye giriyor ve JavaScript'in bu tek iş parçacıklı yapısına rağmen nasıl eşzamansız (asynchronous) çalışabildiğini açıklıyor.

Peki, bu sihirli döngü tam olarak hangi bileşenlerle çalışıyor? Temelde üç ana oyuncumuz var: Call Stack (Çağrı Yığını), Web APIs (Tarayıcı API'leri) veya Node.js C++ API'leri ve Callback Queue (Geri Çağırım Kuyruğu). Ve tabii ki, tüm bu orkestrayı yöneten Event Loop'un kendisi.

Call Stack (Çağrı Yığını)

Öncelikle, Call Stack'e bakalım. Burası, JavaScript motorunun o an hangi fonksiyonu çalıştırdığını takip ettiği yer. Tıpkı bir tabak yığını gibi düşünebilirsiniz; en son eklenen (yani çağrılan) fonksiyon en üstte durur ve ilk o işlenir. Bu, LIFO (Last-In, First-Out) prensibiyle çalışır. Bir fonksiyon çağrıldığında yığına eklenir, işi bittiğinde ise yığından çıkarılır. Eğer bir fonksiyon başka bir fonksiyonu çağırırsa, yeni çağrılan fonksiyon onun üzerine eklenir.

Basit bir örnekle anlatayım:
CODE
12345678910111213
function ikinciIslem() {
  console.log("İkinci işlem tamamlandı.");
}

function birinciIslem() {
  console.log("Birinci işlem başladı.");
  ikinciIslem(); // ikinciIslem çağrılıyor, yığına ekleniyor
  console.log("Birinci işlem bitti.");
}

birinciIslem(); // birinciIslem çağrılıyor, yığına ekleniyor
console.log("Tüm kod bitti.");

Bu kod çalışırken, Call Stack şu adımları izler:
  • Global execution context yığına eklenir.
  • CODE
    1birinciIslem()
    yığına eklenir.
  • CODE
    1console.log("Birinci işlem başladı.")
    çalışır.
  • CODE
    1ikinciIslem()
    yığına eklenir.
  • CODE
    1console.log("İkinci işlem tamamlandı.")
    çalışır.
  • CODE
    1ikinciIslem()
    biter, yığından çıkarılır.
  • CODE
    1console.log("Birinci işlem bitti.")
    çalışır.
  • CODE
    1birinciIslem()
    biter, yığından çıkarılır.
  • CODE
    1console.log("Tüm kod bitti.")
    çalışır.
  • Global execution context biter, yığından çıkarılır.
Senkron kodlar için her şey bu kadar basit. Ancak asenkron işlemler işin içine girdiğinde, Call Stack'i bloke etmemek için farklı bir mekanizmaya ihtiyaç duyuyoruz.

Web APIs (Tarayıcı API'leri) / Node.js C++ API'leri

JavaScript motoru tek iş parçacıklı olabilir, ama tarayıcılar (veya Node.js çalışma zamanı) değildir. Tarayıcılar, DOM API'leri,
CODE
1setTimeout()
,
CODE
1fetch()
,
CODE
1XMLHttpRequest()
gibi asenkron işlemler için bize ek yetenekler sunar. Bunlar, JavaScript motorunun bir parçası değil, tarayıcının veya Node.js'in altında çalışan C++ gibi dillerle yazılmış, ayrı iş parçacıklarında çalışan özelliklerdir.

Şöyle düşünün: JavaScript kodu,
CODE
1setTimeout()
gibi bir asenkron fonksiyonu çağırdığında, bu fonksiyon Call Stack'ten çıkarılır ve ilgili Web API'ye devredilir. Yani JavaScript motoru bu işlemi kendi yapmaz, işi başkasına verir ve kendi yoluna devam eder. Bu sayede ana iş parçacığı bloke olmaz. Örneğin,
CODE
1setTimeout(callback, delay)
çağrıldığında,
CODE
1callback
fonksiyonu belirli bir
CODE
1delay
süresi boyunca Web API'lerinde bekletilir.

Callback Queue (Geri Çağırım Kuyruğu) / Task Queue (Görev Kuyruğu)

Web API'lerinde bekleyen asenkron işlemler tamamlandığında (örneğin,
CODE
1setTimeout
'un süresi dolduğunda veya bir ağ isteği yanıt döndüğünde), bu işlemlerin ilişkili geri çağırım fonksiyonları (callback'leri) hemen Call Stack'e geri dönemez. Neden mi? Çünkü Call Stack meşgul olabilir! İşte bu noktada Callback Queue devreye girer. Tamamlanan asenkron işlemlerin callback'leri, Call Stack boşalana kadar bu kuyrukta bekler. Bu kuyruk, FIFO (First-In, First-Out) prensibiyle çalışır; yani ilk giren callback, ilk işlenmeyi bekler.

Bu kuyruğa "Macrotask Queue" (Makrogörev Kuyruğu) da denir. DOM olayları,
CODE
1setTimeout
,
CODE
1setInterval
,
CODE
1requestAnimationFrame
(bazı durumlarda), I/O işlemleri gibi görevler bu kuyruğa girer.

Event Loop (Olay Döngüsü)

Ve işte sahneye Event Loop çıkıyor! Event Loop, sürekli olarak iki şeye bakar:
  • Call Stack boş mu?
  • Callback Queue'da bekleyen bir görev var mı?
Eğer Call Stack boşsa ve Callback Queue'da bekleyen bir callback varsa, Event Loop bu callback'i kuyruktan alır ve Call Stack'e iter. Böylece callback çalıştırılır. Bu döngü sürekli devam eder, JavaScript motorunun asla boş kalmamasını ve asenkron işlemlerin sırayla işlenmesini sağlar.

Bu döngü sayesinde, JavaScript tek iş parçacıklı olmasına rağmen eşzamanlı gibi bir deneyim sunar. Uzun süren bir ağ isteği gönderdiğinizde, JavaScript motoru o isteğin bitmesini beklemez; Call Stack'ten çıkarır, Web API'sine devreder ve kendi senkron kodunu çalıştırmaya devam eder. Ağ isteği tamamlandığında, callback'i Callback Queue'ya eklenir ve Event Loop, Call Stack boşaldığında onu işler.

Microtask Queue (Mikrogörev Kuyruğu)

Şimdi işler biraz daha ilginçleşiyor. Event Loop'un sadece Callback Queue'ya değil, bir de Microtask Queue'ya baktığını öğrenmemiz gerekiyor. Mikrogörevler, makrogörevlerden (Callback Queue'daki görevler) daha yüksek önceliğe sahiptir. Yani Event Loop, bir makrogörevi alıp Call Stack'e itmeden önce, Microtask Queue'da bekleyen tüm mikrogörevleri bitirir.

Peki neler mikrogörevdir? En bilinenleri:
  • Promise callback'leri (
    CODE
    1.then()
    ,
    CODE
    1.catch()
    ,
    CODE
    1.finally()
    )
  • CODE
    1queueMicrotask()
    ile eklenen görevler
  • CODE
    1MutationObserver
    callback'leri (DOM değişikliklerini izleyen API)
  • CODE
    1process.nextTick()
    (Node.js'e özgü, Promise'lerden bile önce çalışır)

Event Loop çalışma prensibi şu şekilde güncelleniyor:
  • Call Stack'te hiçbir şey yok mu?
  • O zaman Microtask Queue'daki tüm görevleri Call Stack'e sırayla it ve çalıştır.
  • Microtask Queue da boşaldı mı?
  • Şimdi Callback Queue'daki ilk görevi al ve Call Stack'e it.
  • Bu döngü sürekli tekrar eder.
Bu öncelik sırası, Promise'lerin neden bazen
CODE
1setTimeout(fn, 0)
'dan bile önce çalıştığını açıklar.

Bir örnekle bu durumu netleştirelim:
CODE
1234567891011121314
console.log("Senkron 1");

setTimeout(() => {
  console.log("setTimeout callback"); // Macrotask
}, 0);

Promise.resolve().then(() => {
  console.log("Promise then 1"); // Microtask
}).then(() => {
  console.log("Promise then 2"); // Microtask
});

console.log("Senkron 2");

Bu kodun çıktısı ne olur dersiniz?
  • Önce "Senkron 1" yazdırılır. Call Stack'te.
  • CODE
    1setTimeout
    çağrılır, Web API'lerine gider. Callback'i (console.log("setTimeout callback")) belirli bir süre sonra (0ms bile olsa) Macrotask Queue'ya eklenmeyi bekler.
  • CODE
    1Promise.resolve()
    ile bir Promise oluşturulur.
    CODE
    1.then()
    callback'i (console.log("Promise then 1")) Microtask Queue'ya eklenir.
  • CODE
    1console.log("Senkron 2")
    yazdırılır. Call Stack'te.
  • Call Stack boşalır.
  • Event Loop, önce Microtask Queue'ya bakar. "Promise then 1" callback'ini alır, Call Stack'e iter. "Promise then 1" yazdırılır.
  • Bu Promise'in ikinci
    CODE
    1.then()
    callback'i (console.log("Promise then 2")) de Microtask Queue'ya eklenir (çünkü ilk then bitti).
  • Microtask Queue'da başka bir görev var mı? Evet, "Promise then 2". Onu da alır, Call Stack'e iter. "Promise then 2" yazdırılır.
  • Microtask Queue boşalır.
  • Şimdi Event Loop, Macrotask Queue'ya bakar. "setTimeout callback"i alır, Call Stack'e iter. "setTimeout callback" yazdırılır.
Sonuç:
CODE
123456
Senkron 1
Senkron 2
Promise then 1
Promise then 2
setTimeout callback

Gördüğünüz gibi,
CODE
1setTimeout(fn, 0)
bile Promise'lerden sonra çalıştı, çünkü Promise callback'leri mikrogörev olarak daha yüksek önceliğe sahip.

Render Kuyruğu (Tarayıcıda)

Tarayıcı ortamında, Event Loop'un döngüsünde bir de render (ekran çizimi) aşaması vardır. Genellikle, bir makrogörev tamamlandıktan ve tüm mikrogörevler işlendikten sonra, tarayıcı bir sonraki kareyi çizmeden önce DOM değişikliklerini uygular ve ekranı günceller. Bu,
CODE
1requestAnimationFrame
gibi API'lerin neden genellikle DOM manipülasyonları için tercih edildiğini açıklar; çünkü tarayıcının render döngüsüyle senkronize çalışır ve "jank" (takılma) oluşmasını engellemeye yardımcı olur.

Özetle ve Pratikte Anlamı

Event Loop'u anlamak, özellikle performanslı ve yanıt verebilir uygulamalar geliştirmek için kritik öneme sahip.
  • Asla Call Stack'i uzun süreli, bloklayıcı senkron işlemlerle meşgul etmeyin. Eğer yaparsanız, uygulamanız donar.
  • Asenkron işlemleri (
    CODE
    1fetch
    ,
    CODE
    1setTimeout
    ,
    CODE
    1Promise
    tabanlı işlemler) kullanarak ana iş parçacığını serbest bırakın.
  • Promise'lerin ve
    CODE
    1queueMicrotask
    'ın
    CODE
    1setTimeout
    gibi diğer asenkron API'lere göre daha yüksek önceliğe sahip olduğunu unutmayın. Bu, özellikle karmaşık zamanlama senaryolarında önemlidir.
  • Node.js'te
    CODE
    1process.nextTick
    mikrogörevlerden bile daha yüksek önceliklidir ve mevcut Call Stack'teki kod bittikten hemen sonra çalışır, Event Loop'a geçmeden önce. Bu, Node.js'e özgü bir detaydır.
Bu mekanizmayı içselleştirmek, "Neden bu kodum beklediğim sırayla çalışmadı?" gibi soruların cevabını bulmanızı sağlar ve daha öngörülebilir, hatasız JavaScript kodu yazmanıza yardımcı olur.

Peki siz Event Loop ile ilgili hangi senaryolarda zorlandınız veya hangi detaylar sizi şaşırttı? Belki de gözden kaçırdığım, sizin eklemek istediğiniz önemli noktalar vardır... Deneyimlerinizi ve görüşlerinizi merak ediyorum!

Cavab vermək üçün daxil olmalısınız.

0 sitat seçildi