Yazılarımız

OfisData

JAVASCRIPT EVENT LOOP MANTIĞINI ANLAMAK

Sarı JS kare logosu yanında dairesel event loop ok döngüsü call stack kutusu microtask queue ve task queue üç bağlantı noktasıyla bağlanıyor

JavaScript yazan herkesin başına gelir: bir kod yazarsınız, "neden önce bu çalıştı sonra bu?" diye düşünürsünüz. Bir console.log diğerinden önce çıkar; setTimeout 0 milisaniye olsa bile gecikiyormuş gibi davranır; promise resolve olur ama henüz işlem bitmez. Bu davranışların hepsinin altında tek bir mekanizma yatar: event loop.

JavaScript tek iş parçacıklıdır (single-threaded). Yani aynı anda tek satır kod çalıştırır. Ama tarayıcıda yüzlerce işlem aynı anda oluyormuş gibi hissedilir; bir buton tıklanır, bir AJAX cevap döner, bir setInterval çalışır. Bu görünür çoklu paralel davranış event loop sayesinde mümkün olur; mekanizmanın tarayıcı tarafındaki ayrıntıları için web teknolojileri dokümantasyonu derinlemesine bir başvuru sunar.

Aşağıda event loop'un ne olduğunu, call stack, task queue, microtask queue arasındaki sırayı, promise ve async/await davranışlarını gerçekçi örneklerle açıklıyoruz.

JavaScript tek iş parçacıklı mı, paralel mi?

Bu sık karıştırılır. JavaScript dili tek iş parçacıklıdır; ama JavaScript çalışma ortamı (tarayıcı veya Node.js) çok parçalıdır. Yani:

  • Sizin kodunuz tek bir thread'de çalışır
  • Tarayıcı API'ları (setTimeout, fetch, DOM events) ayrı thread'lerde işler
  • İşi biten API çıktısını queue'lara yerleştirir
  • Event loop bu queue'ları izleyip uygun zamanda main thread'e taşır

Bu yapı sayesinde uzun süren bir işlem (örneğin 500 ms AJAX) main thread'i durdurmaz; sonuç hazır olunca event loop sırasıyla yürütür.

Call Stack: sırayla çalışan defter

Call stack, çalışmakta olan fonksiyonların yığını. JavaScript bir fonksiyona girdiğinde stack'in üstüne ekler; fonksiyon bitince stack'ten çıkarır.

function a() {
  console.log("a başladı");
  b();
  console.log("a bitti");
}

function b() {
  console.log("b başladı");
  c();
  console.log("b bitti");
}

function c() {
  console.log("c");
}

a();

Bu kodun call stack davranışı:

  1. a() çağrılır → stack: [a]
  2. "a başladı" yazılır
  3. b() çağrılır → stack: [a, b]
  4. "b başladı" yazılır
  5. c() çağrılır → stack: [a, b, c]
  6. "c" yazılır, c biter → stack: [a, b]
  7. "b bitti" yazılır, b biter → stack: [a]
  8. "a bitti" yazılır, a biter → stack: []

Çıktı sırası: a başladı, b başladı, c, b bitti, a bitti. Bu senkron akış. JavaScript bu akışı bekler; stack temizlenene kadar başka bir şey yapamaz.

Task Queue: bekleyen işler kuyruğu

setTimeout, setInterval, DOM events, fetch (sonucu) gibi asenkron işler bittiğinde callback'leri task queue'ya yerleştirilir. Event loop call stack'i izler; stack boşaldığı anda queue'dan sıradaki callback'i alır ve stack'e koyar.

console.log("1");

setTimeout(() => {
  console.log("3");
}, 0);

console.log("2");

Beklenen sıra: 1, 2, 3. setTimeout 0 milisaniye olsa bile callback hemen çalışmaz; task queue'ya gider, call stack temizlenince çalışır. Bu nedenle "2" "3"ten önce yazılır.

Microtask Queue: öncelikli kuyruk

Promise callback'leri, queueMicrotask, MutationObserver microtask queue'ya gider. Bu kuyruk task queue'dan önceliklidir. Event loop her döngüde önce tüm microtask'leri çalıştırır, sonra bir task'i alır.

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");

Çıktı sırası: 1, 4, 3, 2. Neden?

  1. "1" senkron yazılır
  2. setTimeout task queue'ya gider
  3. Promise.then microtask queue'ya gider
  4. "4" senkron yazılır
  5. Call stack temizlenir
  6. Event loop önce microtask'leri kontrol eder → "3" yazılır
  7. Sonra task'leri kontrol eder → "2" yazılır

Bu öncelik farkı bug yakalamak için kritiktir. "Neden promise her zaman setTimeout'tan önce çalışıyor?" sorusunun cevabı budur.

Event loop mimari diyagramı sol üstte CALL STACK kolon kutusu sağda WEB APIs altında MICROTASK QUEUE ve TASK QUEUE iki yatay sıra ortada dönen ok döngü işareti

async / await aslında nedir?

async/await syntax şekerdir; altta promise vardır. Bir fonksiyonun başına async yazmak, fonksiyonun otomatik olarak promise döndürmesine yol açar. await ifadesi ise "burada beklet" der; ama JavaScript bekletmek için thread'i durdurmaz, fonksiyonu duraklatır ve geri kalanı microtask queue'ya alır.

async function fetchData() {
  console.log("1");
  const veri = await fetch("/api");
  console.log("3");
  return veri;
}

fetchData();
console.log("2");

Çıktı sırası: 1, 2, (sonra fetch tamamlanınca) 3. "await" satırına gelince fonksiyon duraklar; main thread devam eder ("2" yazılır); fetch sonucu microtask olarak gelir ve "3" yazılır.

Tipik event loop hataları

1. for döngüsünde setTimeout sürprizi

// var ile
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Çıktı: 3, 3, 3 (beklenen 0, 1, 2 değil)

// let ile
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Çıktı: 0, 1, 2

var function-scoped olduğu için döngü bittiğinde i = 3 olur ve tüm setTimeout'lar bu değeri görür. let block-scoped olduğu için her döngüde yeni bir i değişkeni oluşur; setTimeout doğru değeri yakalar.

2. Yoğun hesap main thread'i bloke eder

// Bu kod tarayıcıyı dondurur
for (let i = 0; i < 1_000_000_000; i++) {
  // ağır işlem
}
// Sırada bekleyen event'ler (tıklama, animasyon) bloke olur

Çözüm: işlemi parçalara böl, setTimeout veya requestAnimationFrame ile arada main thread'e nefes aldır. Veya Web Worker kullan; ayrı thread'de hesaplama yapar.

3. Promise zincirinin sırası

Promise.resolve()
  .then(() => {
    console.log("A");
    return Promise.resolve();
  })
  .then(() => console.log("B"));

Promise.resolve()
  .then(() => console.log("C"))
  .then(() => console.log("D"));

// Çıktı: A, C, B, D

Microtask queue'ya promise'lar sırayla eklenir. İlk .then içinde return Promise.resolve() yeni microtask oluşturur; bu nedenle B beklenenden geç gelir.

Devtools ile event loop debug

Chrome DevTools'ta event loop sorunlarını ayıklamak için:

Performance paneli

Performance > Record > sayfayı kullan > Stop. Tüm task ve microtask'ler zaman çizgisinde görünür. Uzun süren task'ler kırmızı işaretlenir ("long task"); bunlar main thread'i bloke ediyor demektir.

Async stack traces

DevTools > Settings > Async stack traces aktif. Promise içindeki bir hata oluştuğunda promise zincirini en başa kadar görebilirsiniz. Bu olmadan asenkron hataları takip etmek çok zordur.

Breakpoint with async

Sources panelinde bir promise .then içine breakpoint koyup "Caught/Uncaught" seçeneğini açın. Promise reject olunca otomatik durur.

Performance ipuçları

  • Long task'leri parçalayın. 50 ms'i geçen task INP metriğini bozar; kullanıcı tıkladığında sayfa donmuş hissi verir
  • requestIdleCallback kullanın. Acil olmayan işleri tarayıcı boşken yapın
  • Microtask spam'inden kaçının. Promise zincirinin sonsuz uzaması microtask queue'yu doldurur
  • Web Worker'a taşıyın. Ağır hesaplar (görsel işleme, encrypt, parse) main thread'ten ayrılmalı
  • setInterval yerine setTimeout zinciri. setInterval önceki çağrı bitmese bile tetikler; setTimeout zinciri kontrollü

Sık karşılaşılan event loop soruları

setTimeout(fn, 0) tam olarak ne yapar?

Fonksiyonu task queue'ya yerleştirir; en az 0 ms sonra çalıştırılır. "En az" anahtar kelime; tarayıcılar bazen 4 ms gibi minimum gecikme uygular. Pratikte 0 yazmak "mümkün olan en yakın task slotu" anlamına gelir.

Promise içindeki await ne kadar süre bekler?

Await değişken; await edilen şey hazır olunca devam eder. Ama event loop perspektifinden: await olduğu anda fonksiyon duraklar, main thread devam eder; await edilen promise resolve olunca microtask olarak yerleştirilir.

queueMicrotask ne için kullanılır?

Bir kodun mevcut senkron işlerden sonra ama task queue'dan önce çalışmasını istediğinizde. Promise.resolve().then() ile aynı etki ama daha açık niyet.

Chrome DevTools Performance paneli dark tema flame graph mor scripting yeşil rendering sarı loading task bloklarıyla zaman çizgisi ve kırmızı long task üçgen uyarı işaretleri

Pratik uygulama: kullanıcı deneyimi

Event loop'u anlamak teorik bir konu değil, pratik UX iyileştirme aracıdır:

  • Kullanıcı bir butona tıklayınca anında geri bildirim alır (call stack hemen tepki verir)
  • Sayfa ağır bir işlem yaparken donmaz (uzun işler parçalanır)
  • Animasyonlar takılmaz (microtask priority kullanılır)
  • Form gönderimi sırasında kullanıcı diğer alanları gezebilir (asenkron yapı)

Bu davranışların hiçbiri kendiliğinden gelmez; event loop'a uygun kod yazılınca gelir.

Pratiği Geliştirmek

Event loop, JavaScript'in en derin konularından biri. Bir kez anlaşıldığında asenkron kod, promise hataları ve performans sorunları çok daha şeffaf hale gelir. Sistemli bir öğrenme için JavaScript eğitim programı asenkron programlama ve event loop konularını uygulamalı örneklerle aktarır.

Süreç Akışı

JavaScript event loop; call stack, task queue ve microtask queue arasındaki sırayı yöneten mekanizmadır. JavaScript tek iş parçacıklıdır ama tarayıcı API'ları paraleldir; event loop bu iki dünyayı senkronize eder. Microtask'ler task'lerden önce çalışır; bu nedenle promise her zaman setTimeout'tan önce gelir. async/await syntax altta promise'lar üzerine kurulu; await ifadesi fonksiyonu duraklatır ama thread'i bloke etmez. Bu mantığı kavramak hem zor bug'ları çözmek hem de yüksek performanslı uygulama yazmak için zorunlu temeldir.

 Vimaj