İçeriğe geç
JavaScript

JavaScript Event Loop: Asenkron Çalışma Modeli

Tarık Tunç
JavaScript Event Loop: Asenkron Çalışma Modeli

JavaScript Event Loop: Asenkron Çalışma Modeli

JavaScript tek iş parçacıklı (single-threaded) bir dildir, ancak event loop mekanizması sayesinde asenkron işlemleri verimli şekilde yönetir. Web API'leri, callback kuyruğu, mikro görev kuyruğu ve çağrı yığını (call stack) birlikte çalışarak JavaScript'in eşzamanlı olmayan (non-blocking) doğasını oluşturur. Bu rehberde event loop'un iç çalışma mekanizmasını adım adım inceliyoruz.

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

Call stack, JavaScript motorunun o anda hangi fonksiyonun çalıştığını takip ettiği LIFO (Last In, First Out) veri yapısıdır. Her fonksiyon çağrısı yığına eklenir ve tamamlandığında çıkarılır.

function ucuncu() {
  console.log("Üçüncü fonksiyon");
  // Call stack: [ucuncu, ikinci, birinci, main]
}

function ikinci() {
  console.log("İkinci fonksiyon");
  ucuncu();
}

function birinci() {
  console.log("Birinci fonksiyon");
  ikinci();
}

birinci();

// Call stack değişimi:
// 1. [birinci]
// 2. [birinci, ikinci]
// 3. [birinci, ikinci, ucuncu]
// 4. [birinci, ikinci]  (ucuncu tamamlandı)
// 5. [birinci]           (ikinci tamamlandı)
// 6. []                  (birinci tamamlandı)

// Stack overflow örneği
function sonsuzDongu() {
  return sonsuzDongu(); // RangeError: Maximum call stack size exceeded
}

2. Event Loop Döngüsü

Event loop, call stack boş olduğunda görev kuyruklarından bir sonraki görevi alarak çalıştırır. Her döngüde önce mikro görev kuyruğu tamamen boşaltılır, sonra makro görev kuyruğundan bir görev alınır.

console.log("1 - Script başlangıcı");

// Makro görev: setTimeout
setTimeout(() => {
  console.log("2 - setTimeout callback");
}, 0);

// Mikro görev: Promise.then
Promise.resolve()
  .then(() => {
    console.log("3 - Promise.then (mikro görev 1)");
  })
  .then(() => {
    console.log("4 - Promise.then (mikro görev 2)");
  });

// Mikro görev: queueMicrotask
queueMicrotask(() => {
  console.log("5 - queueMicrotask");
});

console.log("6 - Script sonu");

// Çıktı sırası:
// 1 - Script başlangıcı
// 6 - Script sonu
// 3 - Promise.then (mikro görev 1)
// 5 - queueMicrotask
// 4 - Promise.then (mikro görev 2)
// 2 - setTimeout callback

// Neden bu sıra?
// 1. Senkron kod çalışır (1, 6)
// 2. Call stack boşalır
// 3. Mikro görev kuyruğu tamamen boşaltılır (3, 5, 4)
// 4. Makro görev kuyruğundan bir görev alınır (2)
İpucu: Event loop her döngüde şu sırayı izler: (1) Call stack'i boşalt, (2) tüm mikro görevleri çalıştır, (3) gerekirse render/paint yap, (4) makro görev kuyruğundan bir görev al. Mikro görevler yeni mikro görev eklerse, onlar da aynı döngüde çalıştırılır.

3. Web API'leri ve Görev Kuyrukları

Tarayıcıda setTimeout, fetch, addEventListener gibi API'ler JavaScript motoru dışında çalışır. Tamamlandıklarında callback'leri uygun kuyruğa eklerler.

// Görev türleri ve kuyrukları
// Makro görevler: setTimeout, setInterval, setImmediate, I/O, UI rendering
// Mikro görevler: Promise.then/catch/finally, queueMicrotask, MutationObserver

// Karmaşık örnek
async function demo() {
  console.log("A - async fonksiyon başı");

  await Promise.resolve();
  console.log("B - await sonrası (mikro görev)");

  setTimeout(() => {
    console.log("C - setTimeout içi");

    Promise.resolve().then(() => {
      console.log("D - setTimeout içindeki Promise");
    });
  }, 0);

  console.log("E - async fonksiyon devamı");
}

console.log("F - Script başı");
demo();
console.log("G - Script sonu");

// Çıktı:
// F - Script başı
// A - async fonksiyon başı
// G - Script sonu
// B - await sonrası (mikro görev)
// E - async fonksiyon devamı
// C - setTimeout içi
// D - setTimeout içindeki Promise

// requestAnimationFrame — render öncesi
function animasyon() {
  requestAnimationFrame((timestamp) => {
    console.log("RAF: render öncesi çalışır");
    // DOM güncellemeleri burada yapılmalı
  });
}

// MutationObserver — mikro görev olarak çalışır
const gozlemci = new MutationObserver((mutasyonlar) => {
  console.log("DOM değişikliği tespit edildi (mikro görev)");
});

gozlemci.observe(document.body, { childList: true });

4. setTimeout(fn, 0) Yanılgısı

setTimeout(fn, 0) "hemen çalıştır" anlamına gelmez. Callback'i makro görev kuyruğuna ekler ve en erken bir sonraki event loop döngüsünde çalışır.

// setTimeout(fn, 0) gerçek davranışı
const baslangic = performance.now();

setTimeout(() => {
  const gecen = performance.now() - baslangic;
  console.log(`setTimeout(0) gerçek gecikme: ${gecen.toFixed(2)}ms`);
  // Genellikle 4ms'den fazla (tarayıcı minimum gecikme)
}, 0);

// Ağır senkron işlem
for (let i = 0; i < 1000000; i++) {
  // Bu döngü tamamlanmadan setTimeout callback'i çalışmaz
}

const gecen = performance.now() - baslangic;
console.log(`Senkron işlem süresi: ${gecen.toFixed(2)}ms`);

// Uzun süren işlemi parçalama
function uzunIslem(veri, index = 0) {
  const PARCA_BOYUTU = 1000;
  const bitis = Math.min(index + PARCA_BOYUTU, veri.length);

  for (let i = index; i < bitis; i++) {
    isleOge(veri[i]);
  }

  if (bitis < veri.length) {
    // Sonraki parçayı event loop'a bırak (UI donmaz)
    setTimeout(() => uzunIslem(veri, bitis), 0);
  } else {
    console.log("Tüm işlem tamamlandı");
  }
}
Uyarı: Uzun süren senkron işlemler event loop'u bloke eder ve UI'ın donmasına neden olur. 50ms'den uzun sürecek hesaplamaları Web Worker'a taşıyın veya parçalara bölerek setTimeout ile event loop'a alan tanıyın.

5. Node.js Event Loop Farkları

Node.js event loop'u tarayıcıdan farklı fazlara sahiptir. process.nextTick, setImmediate ve I/O callback'leri gibi Node.js'e özgü mekanizmalar vardır.

// Node.js event loop fazları:
// 1. timers (setTimeout, setInterval)
// 2. pending callbacks (I/O hataları)
// 3. idle, prepare (dahili)
// 4. poll (I/O callbacks)
// 5. check (setImmediate)
// 6. close callbacks

// process.nextTick vs Promise.then
process.nextTick(() => {
  console.log("nextTick — mikro görevlerden bile önce");
});

Promise.resolve().then(() => {
  console.log("Promise.then — mikro görev");
});

// setImmediate vs setTimeout(fn, 0)
setTimeout(() => {
  console.log("setTimeout");
}, 0);

setImmediate(() => {
  console.log("setImmediate");
});
// Sıra belirsiz (I/O callback içinde ise setImmediate her zaman önce)
Bilgi: Node.js'te process.nextTick callback'leri, Promise mikro görevlerinden bile önce çalışır. Ancak aşırı kullanımı I/O işlemlerini açlığa (starvation) uğratabilir. Mümkünse queueMicrotask veya setImmediate tercih edin.

Sonuç

Event loop, JavaScript'in asenkron çalışma modelinin kalbidir. Call stack, Web API'leri, mikro görev kuyruğu ve makro görev kuyruğu arasındaki etkileşimi anlamak, asenkron kodun davranışını tahmin etmenizi ve performans sorunlarını teşhis etmenizi sağlar. Bu bilgi, setTimeout, Promise, async/await ve DOM event'leri ile çalışırken doğru kararlar vermeniz için kritik öneme sahiptir.

Kaynaklar

İlgili Yazılar