Serimizin önceki yazılarında donanım seviyesindeki I/O süreçlerini, V8 motorunun bellek mimarisini (Call Stack & Memory Heap) ve kod seviyesinde asenkron yapıların (Callback, Promise, Async/Await) evrimini inceledik.
Ancak ortada yanıtlanmayı bekleyen devasa bir soru var:
setTimeoutveyafetchgibi asenkron bir işlem başlattığımızda, JavaScript motoru (V8) bunu Call Stack'ten çıkardıktan sonra arka planda tam olarak ne olur? Bu işlemler bittiğinde JavaScript hangi kurala göre birPromisecevabınısetTimeoutcevabından daha önce çalıştırır?
İşte bu yazıda, tarayıcının görünmeyen kahramanlarını, görev kuyrukları arasındaki "öncelik savaşlarını" ve Event Loop'un milisaniye milisaniye çalışan karar algoritmasını mercek altına alıyoruz.
1. Web APIs Nedir? (JavaScript Motorunun Dışındaki Dünya)
Geliştiricilerin en sık düştüğü yanılgılardan biri; setTimeout, fetch, document.addEventListener veya localStorage gibi yapıların bizzat JavaScript dilinin (V8 motorunun) kendi öz fonksiyonları olduğunu sanmaktır.
Gerçek şudur: JavaScript motoru (V8) sadece senkron kodları okur ve Call Stack üzerinde çalıştırır. Zaten V8 tek başına ne zamanlayıcıdan (timer) anlar ne de ağ (network) kartından!
Peki Web APIs Tam Olarak Nedir?
Web APIs; tarayıcının (Chrome, Firefox, Safari vb.) C++ ile yazılmış, arka planda işletim sistemi ve donanım imkânlarını kullanan ekstra yetenek paketleridir. (Node.js tarafındaki karşılığı ise C++ bindings ve libuv kütüphanesidir.)
2. Microtask Queue vs. Macrotask (Task) Queue
Arka plandaki işi (zamanlayıcı, ağ isteği veya Promise) biten her asenkron görev kuyruğa atılır. Ancak JavaScript ekosisteminde tüm görevler eşit değildir. İki farklı bekleme salonu vardır ve aralarında katı bir hiyerarşi bulunur.
2.1 Microtask Queue (Birinci Sınıf Öncelik)
Microtask kuyruğu, doğrudan JavaScript dil standardının (ECMAScript) iç mekanizmalarından kritik görevleri barındırır.
İçeriği:
Promisezincirleri (.then(),.catch(),.finally()),async/awaitsonrasındaki devam kodları,queueMicrotask()ve DOM değişikliklerini izleyenMutationObserver.
Event Loop, Call Stack boşaldığı an ilk olarak Microtask kuyruğuna bakar. Burada kavramamız gereken en kritik nokta şudur: Henüz yanıtı gelmemiş, arka planda bekleyen asenkron işler değil; o an yanıtı gelip Microtask kuyruğuna fiilen girmiş (çözümlenmiş / fulfilled / rejected) olan tüm görevler işleme alınır.
Event Loop bu kuyruğa bir kez girdi mi; kuyrukta bekleyen tüm hazır Microtask'leri ve o görevler çalışırken zincirleme olarak yeni çözümlenip kuyruğa eklenen diğer Microtask'leri tamamen sıfırlayana kadar Macrotask kuyruğuna veya tarayıcının ekran yenileme (UI Render) adımına asla geçmez.
(Eğer bir Promise henüz çözümlenmediyse veya bir
asyncişlem ağdan yanıt bekliyorsa, o henüz Microtask kuyruğuna düşmediği için Event Loop onu beklemez; kuyruğa fiilen düşmüş hazır işleri eritip yoluna devam eder.)
2.2 Macrotask Queue / Task Queue (Standart Öncelik)
Macrotask kuyruğu, doğrudan V8 motorunun değil; tarayıcı veya Node.js gibi çalışma ortamlarının (Runtime Environment) tetiklediği daha genel ve sistemsel görevleri barındırır.
İçeriği:
setTimeout,setInterval,setImmediate(Node.js), I/O (disk/ağ) işlemleri ve kullanıcı etkileşimleri (DOMclick,scrollolayları).
Macrotask kuyruğunda da mantık benzerdir: setTimeout(fn, 2000) gibi bir işlem başlatıldığında, o 2 saniyelik süre boyunca görev Macrotask kuyruğunda yer kaplamaz; tarayıcının Web APIs (C++) katmanında bekletilir. Yalnızca süresi dolan veya olayı gerçekleşen (örneğin kullanıcı butona tıkladığı an) görevler Macrotask kuyruğuna fiilen düşer.
Microtask kuyruğu tamamen boşaldığında Event Loop Macrotask alanına uğrar. Buradaki çalışma mantığı ise Microtask'ten tamamen farklıdır:
Tek Görev Limiti: Macrotask kuyruğunda hazırda bekleyen 10 tane görev olsa bile, Event Loop SADECE 1 ADET (TEK BİR) görevi alır ve çalıştırılmak üzere Call Stack'e taşır.
O tek görev biter bitmez Event Loop durur. Tarayıcıya ekranı yeniden çizme (
UI Render / Re-paint) fırsatı tanır ve kuyruğa yeni bir Microtask (Promisevb.) düşüp düşmediğini kontrol etmek için anında başa döner.
Macrotask İçinden Doğan Microtask'ler
"Peki Event Loop neden bekleyen 10 Macrotask'i peş peşe çalıştırmak yerine her 1 Macrotask'ten sonra durup başa dönüyor?" sorusunun cevabı şudur: Bir Macrotask çalışırken, onun içerisinde yeni bir Microtask doğabilir!
Örneğin, bir setTimeout callback'i yürütülürken fonksiyonun içerisinde bir fetch() isteği atılabilir veya bir Promise çözümlenebilir:
setTimeout(() => { // 1. Macrotask çalışmaya başladı
console.log("Macrotask çalışıyor...");
Promise.resolve().then(() => {
// 2. Bu Macrotask'in İÇİNDE yeni bir Microtask doğdu!
console.log("İçerideki Microtask!");
});
}, 0);İşte bu yüzden Event Loop her Macrotask'ten sonra durmak zorundadır. Bekleyen diğer Macrotask'lere geçmeden önce, "Acaba az önce çalıştırdığım tek bir Macrotask'in içinden yeni bir Promise (Microtask) doğdu mu?" diye başa dönüp Microtask kuyruğunu kontrol eder.
Eğer yeni bir Microtask doğmuşsa önce onu temizler; Microtask kuyruğu tamamen boşsa sıradaki 2. Macrotask'i almak üzere geri gelir.
3. Event Loop: Çalışma Zamanı (Runtime) Mekanizması
JavaScript çalışma zamanı ortamında (Browser Runtime); yürütme motoru (V8), tarayıcı API’leri (Web APIs) ve görev kuyrukları (Task Queues) birbirinden bağımsız modüller olarak çalışır. Bu modüller arasındaki veri akışını, görev önceliklerini ve yürütme sırasını HTML5 spesifikasyonunda tanımlanan Event Loop (Olay Döngüsü) yönetir.
3.1 HTML5 Spesifikasyonuna Göre Event Loop Algoritması
WHATWG (Web Hypertext Application Technology Working Group) standartlarına göre bir Event Loop döngüsü (iteration), kesintisiz olarak şu adımları sırasıyla yürütür:
while (eventLoop.waitForTask()) {
// 1. Call Stack denetimi
if (callStack.isEmpty()) {
// 2. Microtask kuyruğunu tamamen boşalt
while (!microtaskQueue.isEmpty()) {
let microTask = microtaskQueue.dequeue();
callStack.push(microTask);
// Görev Call Stack üzerinde senkron yürütülür
}
// 3. Arayüz Güncelleme (UI Rendering)
if (shouldRenderFrame()) {
updateRendering(); // Style, Layout, Paint adımları
}
// 4. Macrotask kuyruğundan SADECE 1 görev al
if (!macrotaskQueue.isEmpty()) {
let macroTask = macrotaskQueue.dequeue();
callStack.push(macroTask);
// Görev Call Stack üzerinde senkron yürütülür
}
}
}
Not (İşlemci Mimarisi): Yukarıdaki
whiledöngüsü bir busy-waiting (işlemciyi %100 yükte tutan sonsuz döngü) değildir. Kuyruklarda çalıştırılacak görev bulunmadığında Event Loop, işletim sisteminin olay bildirim mekanizmaları (epoll,kqueue,IOCP) aracılığıyla uyku (block/sleep) moduna geçer. Yeni bir olay (tıklama, ağ yanıtı veya zamanlayıcı dolması) gerçekleştiğinde işletim sistemi tarafından tekrar uyandırılır.
3.2 Adım Adım Yürütme Mimarisi
HTML5 spesifikasyonunun "Processing Model" kuralları gereği, Event Loop’un karar mekanizması şu net adımlarla işler:
Adım 1: Call Stack Kontrolü
Event Loop her döngünün başında V8 motorunun Call Stack alanını denetler. Eğer Call Stack üzerinde yürütülmekte olan aktif bir JavaScript fonksiyonu varsa, Event Loop hiçbir kuyruktan görev çekmez. Yürütmenin tamamlanmasını ve Stack’in 0 boyutuna inmesini bekler.
Adım 2: Microtask Kuyruğunun Kontrolü (Perform Microtask Checkpoint)
Call Stack boşaldığı anda Event Loop kontrol noktasını (checkpoint) çalıştırır ve doğrudan Microtask Queue alanına yönelir.
Kuyrukta bekleyen tüm
Promisegeri bildirimleri (.then,.catch,.finally),queueMicrotask()çağrıları veMutationObservercallback’leri sırayla Call Stack’e taşınır.Tümünü Eritme İlkesi: Döngü, kuyruktaki tüm Microtask’ler tükenene kadar devam eder. Bu sırada yürütülen bir Microtask içerisinden yeni bir Microtask doğarsa, o da aynı kontrol adımında yürütülür. Microtask kuyruğu tamamen sıfırlanmadan bir sonraki adıma geçilmez.
Adım 3: Arayüz Güncellemesi (UI Rendering)
Microtask kuyruğu tamamen boşaldıktan sonra tarayıcı, ekranın yeniden çizilme ihtiyacını değerlendirir.
Eğer bir ekran karesi (frame) yenileme zamanı geldiyse; CSS hesaplamaları (Style), düzen yapılandırması (Layout) ve piksel çizimi (Paint) adımları bu aşamada yürütülür.
Sonsuz Microtask kilitlenmelerinin (örneğin sürekli kendini çağıran bir
Promisezincirinin) sayfayı dondurma sebebi, Event Loop’un bu 3. adıma hiçbir zaman geçememesidir.
Adım 4: Tekil Macrotask Yürütmesi
Arayüz adımı geçildikten veya ekran yenileme gerekmediği anlaşıldıktan sonra Event Loop Macrotask Queue (Task Queue) alanına uğrar.
Kuyrukta bekleyen
setTimeout,setInterval, I/O geri bildirimleri veya DOM olaylarından (click,scroll) yalnızca 1 adet (en eski) görev seçilir ve Call Stack’e aktarılır.Diğer bekleyen Macrotask’ler kuyrukta kalmaya devam eder.
Adım 5: Döngü Başına Dönüş
Seçilen tekil Macrotask Call Stack üzerinde çalışıp tamamlandıktan sonra, Event Loop anında Adım 1’e geri döner.
Kuyrukta bekleyen 2. bir Macrotask olmasına bakılmaksızın başa dönülmesinin nedeni; az önce çalıştırılan tekil Macrotask’in içerisinde yeni bir Microtask doğurmuş olma ihtimalidir. Event Loop sıradaki Macrotask’e geçmeden önce mutlaka başa döner, Call Stack’i denetler ve Microtask kuyruğunu yeniden temizler.
📌 Mimari Not: Tarayıcı, Node.js ve Donanım Katmanı Ayrımı
Bu makalede incelediğimiz Event Loop modeli, HTML5 (WHATWG) spesifikasyonunu ve tarayıcı (Browser Runtime) mimarisini esas almaktadır. Ancak sistem mimarisi açısından şu iki sınırı net çizmek gerekir:
Node.js Çalışma Zamanı Farkı (libuv): Node.js tarafında bir UI/Rendering (ekran çizimi) adımı bulunmaz. Node.js, Event Loop yönetimi için tarayıcı spesifikasyonunu değil, C tabanlı libuv kütüphanesini kullanır. Bu nedenle Node.js Event Loop'u; Timers, Pending I/O, Poll, Check (setImmediate), Close Callbacks gibi kendine has fazlardan (phases) oluşur. Ancak
Microtask Queueönceliği ve Mantığı her iki çalışma zamanında da aynı prensiple işler.Web APIs ve Donanım/İşlemci Bağlantısı: JavaScript tek iş parçacıklıdır (single-threaded); fakat çalışma zamanını barındıran tarayıcı (örneğin Chromium) çok iş parçacıklıdır (multi-threaded).
setTimeout,fetchveya DOM olayları işletim sisteminin çekirdek (kernel) seviyesindeki I/O ve Timer sistemlerine devredilir (Linux'taepoll, macOS'takqueue, Windows'taIOCP).İşlemci (CPU) düzeyinde bir busy-waiting yaşanmaz. Web APIs tarafındaki asenkron işler tamamlandığında, işletim sistemi seviyesindeki bu kesilmeler (interrupts) ilgili callback fonksiyonunu Macrotask/Microtask kuyruklarına fırlatır ve Event Loop'u "uyandırır".
JavaScript tek iş parçacıklı (single-threaded) bir dildir; ancak tarayıcının çok iş parçacıklı (multi-threaded) C++ katmanı ve HTML5 standartlarıyla tanımlanmış Event Loop algoritması sayesinde kesintisiz bir asenkron akış sunar.
Call Stack'in boşalma anlarını, Microtask kuyruğunun eksiksiz temizlenme kuralını ve Macrotask'lerin tekil yürütme prensibini bilmek; hem performans hatalarını kökten çözmenizi hem de asenkron koda tamamen hakim olmanızı sağlar.
