JAVASCRİPT EVENT LOOP NEDİR? MİCROTASK/MACROTASK VE ASYNC DAVRANIŞI
JavaScript tek iş parçacıklı (single-threaded) görünür ama tarayıcı ve Node.js ortamı sayesinde aynı anda pek çok iş yürüyormuş gibi davranır. Bu “sihir” hissinin arkasında event loop, yani olay döngüsü vardır: hangi işin ne zaman çalışacağını belirleyen, kuyrukları yöneten ve akışı düzenleyen mekanizma.
Promise’ler neden setTimeout’tan önce çalışır? async/await neden “durup sonra devam ediyormuş” gibi görünür? Bazen “kodu sırayla yazdım ama çıktı sırası farklı” şaşkınlığını yaşatan şey, microtask ve macrotask ayrımıyla birlikte event loop’un adımlarıdır.
Bu yazıda event loop’u parça parça sökeceğiz: call stack, Web API’ler/host API’ler, task queue, microtask queue ve render aşaması. Sonunda, gerçekçi örneklerle çıktı sırasını tahmin edebilir ve performans sorunlarında doğru noktayı yakalayabilirsin. Daha kapsamlı pratik için JavaScript eğitimi içeriğine de göz atabilirsin.
Primary Keyword: JavaScript Event Loop Mantığı
JavaScript event loop mantığı, temelde “şu an çalışan kod bitti mi?” sorusuna göre ilerler. JavaScript’in yürüttüğü senkron kodlar call stack üzerinde çalışır. Stack boşaldığında event loop, sıradaki işleri kuyruklardan alıp tekrar stack’e taşır.
Burada kritik nokta şu: JavaScript motoru tek başına tüm işi yapmaz. Tarayıcıda zamanlayıcılar, DOM olayları, ağ istekleri gibi işlemler host ortamında (Web API’ler) takip edilir. Node.js tarafında da benzer şekilde libuv ve ilgili altyapı devrededir. JavaScript tarafı ise “hangi callback şimdi çalışacak?” kararını event loop ile verir.

Call Stack: Sıradaki Kod Nerede Bekler?
Call stack, fonksiyon çağrılarının üst üste dizildiği yerdir. Bir fonksiyon başka bir fonksiyonu çağırdığında yeni bir frame eklenir; iş bitince frame çıkar. Stack doluyken event loop “araya girmez”; önce stack’in boşalmasını bekler. Bu yüzden uzun süren senkron işlemler arayüzü dondurabilir.
Host API’ler: Zamanlayıcılar ve IO Kimin Elinde?
setTimeout, DOM event’leri, fetch gibi işlemler host ortamında takip edilir. Süre dolduğunda veya IO tamamlandığında ilgili callback “çalıştırılmaya hazır” hâle gelir; ama hemen stack’e girmez. Önce ilgili kuyruğa (genellikle task queue) bırakılır.
Microtask ve Macrotask Nedir?
Event loop’un en çok kafa karıştıran kısmı, iki farklı kuyruk önceliğidir: microtask queue (mikro görevler) ve macrotask queue (task queue olarak da anılır). Genel kural: stack boşalınca önce microtask’lar tüketilir, sonra sıradaki macrotask alınır. Yani microtask’lar daha “acil” kabul edilir.
- Microtask: Promise.then/catch/finally, queueMicrotask, MutationObserver callback’leri.
- Macrotask: setTimeout, setInterval, MessageChannel, I/O callback’leri, bazı UI event callback’leri.
Microtask Queue Önceliği: Neden Promise Önce Gelir?
Bir Promise çözüldüğünde, ardından gelen then zinciri microtask kuyruğuna eklenir. Event loop, mevcut senkron iş bitince önce microtask kuyruğunu “tamamen” boşaltır. Bu yüzden aynı tur içinde Promise callback’leri çoğunlukla setTimeout callback’lerinden önce çalışır.
Macrotask Queue: Zamanlayıcılar ve Olaylar Ne Zaman Koşar?
setTimeout(0) yazmak “hemen çalıştır” anlamına gelmez; “en erken uygun macrotask turunda çalıştır” demektir. Stack boşalır, microtask’lar biter, sonra bir macrotask seçilir. Eğer araya çok sayıda microtask eklenirse, zamanlayıcıların geciktiğini görebilirsin.
Örnek 1: Çıktı Sırası Nasıl Tahmin Edilir?
Aşağıdaki örnek, microtask ve macrotask önceliğini net gösterir. Burada primary amaç “çalışma sırası”nı akılda kalıcı şekilde görmek: önce senkron log’lar, sonra microtask, en son macrotask.
console.log('A');
setTimeout(() => {
console.log('B (setTimeout)');
}, 0);
Promise.resolve()
.then(() => console.log('C (promise then 1)'))
.then(() => console.log('D (promise then 2)'));
console.log('E');Beklenen sıra genellikle şöyledir: A, E, C, D, B. Çünkü senkron kod (A ve E) stack’te biter; ardından microtask kuyruğundaki then’ler tüketilir; sonra macrotask olan setTimeout callback’i çalışır. Bu örnek, promise then sırası ile setTimeout sırası arasındaki farkı pratikte gösterir.

Async/Await Davranışı: Nerede Durur, Nerede Devam Eder?
async/await davranışı, çoğu zaman “sanki thread durdu” gibi algılanır. Aslında await, bir Promise’in sonucunu beklerken fonksiyonun devamını microtask kuyruğuna planlar. Yani await sonrası satırlar, mevcut senkron akış tamamlandıktan sonra microtask olarak çalışmaya devam eder.
Await Sonrası Kod Neden Sonra Çalışır?
await edilen değer bir Promise ise, çözülme anında devam kısmı sıraya alınır. Eğer await edilen şey zaten çözülmüş bir Promise ise bile, devam satırları yine de senkron akışın “tam ortasına” girmez; microtask aşamasında çalışır. Bu, tahmin edilebilirlik ve tutarlılık sağlar.
Örnek 2: Async/Await ve Promise Birlikte
async function run() {
console.log('1');
await Promise.resolve();
console.log('2 (after await)');
Promise.resolve().then(() => console.log('3 (then)'));
console.log('4');
}
console.log('0');
run();
console.log('5');Bu tip örneklerde 0 ve 5 senkron akışın parçalarıdır. run içindeki 1 yazılır, sonra await yüzünden fonksiyonun devamı microtask olarak planlanır. Ardından ana akış 5’i basar. Sonraki turda microtask’lar devreye girer ve 2 ile diğer microtask’lar sırayla çalışır. Burada dikkat: “then” microtask’ı, nerede eklendiğine bağlı olarak farklı bir sırada görünebilir; bu yüzden microtask kuyruğuna eklenme anı kritik bir detaydır.
Render ve UI Güncellemeleri: Event Loop Nerede Çizim Yapar?
Tarayıcıda event loop turları arasında render aşaması devreye girebilir. DOM değişiklikleri yapılmışsa, stil hesaplama, layout ve paint gibi aşamalar gerçekleşir. Ancak microtask kuyruğu sürekli dolu tutulursa, render gecikebilir; bu da “sayfa donuyor” hissi yaratabilir.
Uzun Synchronous İşler ve “Jank” Etkisi
Uzun süren senkron döngüler stack’i meşgul eder ve kullanıcı etkileşimini geciktirir. Burada amaç, işleri parçalayarak kuyruklara yaymaktır. Örneğin ağır bir işi parçalara bölüp setTimeout veya requestAnimationFrame gibi mekanizmalarla dağıtmak, UI akıcılığını koruyabilir.
Gerçek Hayat Senaryoları ve İpuçları
Event loop bilgisinin asıl faydası, “neden gecikti?” sorusunu doğru yerde aramaktır. Aşağıdaki noktalar, günlük geliştirmede çok işe yarar:
- Performans: Microtask’ları aşırı büyütmek render’ı engelleyebilir; zincir halinde then çağrıları kontrolsüz büyümesin.
- Debug: Çıktı sırası şaşırıyorsa önce “hangi callback hangi kuyruğa gidiyor?” sorusunu sor.
- Stabil davranış: setTimeout(0) ile “hemen” beklemek yerine, akışın gerçekten bir sonraki macrotask turuna geçeceğini varsay.
- Async zincirler: await sonrası kodun microtask olduğunu unutma; UI güncellemesini görmek istiyorsan render fırsatı tanı.
QueueMicrotask Ne Zaman Mantıklı?
Bir işi “hemen senkron” çalıştırmak istemezsin ama setTimeout ile bir tur gecikmesini de istemeyebilirsin. Bu durumda queueMicrotask benzeri yöntemler, mevcut senkron akış biter bitmez devreye girerek daha hızlı bir planlama sağlar. Yine de microtask’ların render’ı erteleyebileceğini akılda tutmak gerekir.
Node.js Notu: Fazlar ve Öncelikler
Node.js tarafında event loop fazları (timers, poll, check gibi) daha detaylı bir akış sunar. Ancak microtask mantığı genel olarak korunur: Promise callback’leri microtask olarak önceliklidir. Node tarafında ayrıca process.nextTick gibi daha yüksek öncelikli mekanizmalar da bulunur; bunlar kontrolsüz kullanıldığında starvation (diğer işlerin aç kalması) etkisi oluşturabilir.

Özet: Doğru Zihinsel Model
Event loop’u anlamak için üç temel cümle yeterli: (1) Senkron kod stack’te bitmeden hiçbir callback çalışmaz. (2) Stack boşalınca önce microtask kuyruğu tamamen tüketilir. (3) Sonra bir macrotask seçilir ve döngü devam eder. Tarayıcıda bunların arasına render fırsatları girer.
Bu zihinsel modelle, Promise ve setTimeout sırasını, async/await devamının neden sonra geldiğini ve performans darboğazlarının nerede oluştuğunu çok daha rahat çözersin. Artık bir log çıktısı gördüğünde “hangi kuyruğa gitti?” sorusu otomatik çalışmaya başlayacak.


