İçeriğe geç
Educora
İleri22 dk17 / 27

Olay döngüsü ve eşzamanlılık

Motorun içine bak: çağrı yığını, görev kuyruğu ve mikro görev kuyruğu. `setTimeout`, Promise ve `await`'in tam çalışma sırasını tahmin etmeyi öğren.

Kendini test et
Bu derste öğreneceklerin
  • Çağrı yığınını, görev kuyruğunu ve mikro görev kuyruğunu tanımlamak
  • Senkron kod, setTimeout, Promise ve await karışık kodun çıktı sırasını tahmin etmek
  • Sıralı ve paralel asenkron kod arasında seçim yapmak: Promise.all, allSettled, race, any

Mülakatların klasik sorusu: “Bu kod hangi sırayla ne yazdırır?” ve önünde bir console.log, setTimeout(..., 0) ve Promise.then karışımı. Deneyimli geliştiriciler bile sık sık yanılır. JavaScript tek bir iş parçacığında çalışır, ama binlerce tıklamayı, isteği ve zamanlayıcıyı yönetir. Sır olay döngüsündedir (event loop). Üç yapıyı, yani çağrı yığınını, görev kuyruğunu ve mikro görev kuyruğunu anladığında bu bulmacalar basit bir hesaba dönüşür.

Çağrı yığını

Çağrı yığını, şu anda çalışan fonksiyonların listesidir. Bir fonksiyon çağrıldığında yığının üstüne yeni bir “çerçeve” konur, fonksiyon döndüğünde kaldırılır. JavaScript'in tek bir yığını vardır, yani aynı anda yalnızca tek bir iş yapar. Yığın meşgulken başka hiçbir şey olamaz: tıklamalar işlenmez, zamanlayıcılar çalışmaz, sayfa yeniden çizilmez.

JavaScript
function third() {
  console.log('3. third is on top of the stack');
}

function second() {
  console.log('2. second calls third');
  third();
  console.log('4. back in second');
}

function first() {
  console.log('1. first calls second');
  second();
  console.log('5. back in first');
}

first();
▸ Beklenen çıktı
1. first calls second
2. second calls third
3. third is on top of the stack
4. back in second
5. back in first
Yığın: first → second → third. Her fonksiyon ancak içindeki çağrı bittikten sonra devam eder. Sonsuz özyineleme yığını doldurur ve motor bir hata fırlatır.

Görevler ve mikro görevler

Asenkron bir iş (zamanlayıcı, ağ yanıtı, tıklama) bittiğinde callback'i hemen çalışmaz; bir kuyrukta bekler. İki ana kuyruk vardır. Görev kuyruğuna (macrotask) setTimeout, setInterval, kullanıcı olayları ve mesajlar girer. Mikro görev kuyruğuna ise Promise callback'leri (then, catch, finally), await'ten sonraki kod ve queueMicrotask girer. Olay döngüsü bunları şöyle işler:

  1. 1
    Mevcut kodu sonuna kadar çalıştır

    Betik (ya da bir görev) yığın boşalana kadar çalışır.

  2. 2
    TÜM mikro görevleri çalıştır

    Bu sırada eklenen yeni mikro görevler dahil, kuyruk tamamen boşalana kadar.

  3. 3
    Gerekirse sayfayı çiz

    Tarayıcı değişiklikleri ekrana yansıtır.

  4. 4
    Kuyruktan TEK bir görev al

    Örneğin bir zamanlayıcı callback'i; ardından 1. adıma dön.

JavaScript
console.log('A: script start');

setTimeout(() => console.log('B: timeout'), 0);

Promise.resolve()
  .then(() => console.log('C: promise 1'))
  .then(() => console.log('D: promise 2'));

queueMicrotask(() => console.log('E: microtask'));

console.log('F: script end');
▸ Beklenen çıktı
A: script start
F: script end
C: promise 1
E: microtask
D: promise 2
B: timeout
Önce senkron A ve F. Sonra mikro görevler: C ve E zaten kuyruktaydı, D ise C bitince eklendi. B zamanlayıcısı en sonda, ayrı bir görev olarak.

Kuyrukta async/await

await yalnızca kendi async fonksiyonunu duraklatır; await'ten sonraki kod bir mikro görev olarak devam eder. Dikkat: ilk await'e kadar olan kod, fonksiyon çağrıldığı anda senkron olarak çalışır.

JavaScript
async function load() {
  console.log('2. load starts: synchronous part');
  await null;
  console.log('4. after await: microtask');
}

console.log('1. script start');
setTimeout(() => console.log('6. timeout: task'), 0);
load();
Promise.resolve().then(() => console.log('5. then: microtask'));
console.log('3. script end');
▸ Beklenen çıktı
1. script start
2. load starts: synchronous part
3. script end
4. after await: microtask
5. then: microtask
6. timeout: task

Mikro görev kuyruğu bir sonraki görevden önce tamamen boşaltılır. Bu yüzden bir zamanlayıcının içinde oluşturulan Promise, bir sonraki zamanlayıcıdan önce çalışır. Aynı nedenle kendini sonsuza dek kuyruğa ekleyen bir mikro görev, sayfayı sonsuz bir while döngüsü gibi dondurur.

JavaScript
setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => console.log('-> microtask inside timeout 1'));
}, 0);

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

Promise.resolve().then(() => {
  console.log('microtask 1');
  setTimeout(() => console.log('timeout 3, added by microtask 1'), 0);
});
▸ Beklenen çıktı
microtask 1
timeout 1
-> microtask inside timeout 1
timeout 2
timeout 3, added by microtask 1

Sıralı ve paralel

JavaScript tek iş parçacıklıdır, ama beklemeler üst üste binebilir: ağ isteklerini ve zamanlayıcıları arka planda tarayıcı (ya da Node.js) izler. Buna eşzamanlılık (concurrency) denir: aynı anda birçok iş “sürmektedir”, ama her an yalnızca bir JavaScript parçası çalışır. Sık yapılan hata: döngü içindeki await, istekleri birbiri ardına gönderir. İşler bağımsızsa hepsini birlikte başlat ve Promise.all ile bekle.

JavaScript
const task = (label, ms) =>
  new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${label} done`);
      resolve(label);
    }, ms);
  });

console.log('Sequential:');
await task('A (200 ms)', 200);
await task('B (100 ms)', 100);

console.log('Parallel:');
const results = await Promise.all([task('A (200 ms)', 200), task('B (100 ms)', 100)]);
console.log(results);
▸ Beklenen çıktı
Sequential:
A (200 ms) done
B (100 ms) done
Parallel:
B (100 ms) done
A (200 ms) done
[ 'A (200 ms)', 'B (100 ms)' ]
Sıralı sürüm ~300 ms, paralel sürüm ~200 ms sürer; paralel çalıştırmada B önce biter, ama Promise.all sonuçları dizideki sırayla döndürür. En üst düzeyde await burada, modüllerde olduğu gibi çalışır.
MetotNe zaman sonuçlanırSonuç
Promise.allhepsi başarılı olunca ya da ilk hatadadeğerler dizisi ya da ilk hata
Promise.allSettledhepsi bitince; asla reddedilmez{ status, value } / { status, reason } dizisi
Promise.raceilki sonuçlanınca (başarı ya da hata)en hızlı sonuç
Promise.anyilk başarıda; hepsi başarısızsa hatailk başarılı değer ya da AggregateError
JavaScript
const ok = (value, ms) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (reason, ms) =>
  new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));

const settled = await Promise.allSettled([ok('menu', 100), fail('no stock', 50)]);
console.log(settled.map((result) => result.status));

console.log(await Promise.any([fail('server 1 down', 50), ok('server 2', 100)]));
console.log(await Promise.race([ok('fast', 50), ok('slow', 150)]));

try {
  await Promise.all([ok('menu', 100), fail('no stock', 50)]);
} catch (error) {
  console.log('Promise.all failed:', error.message);
}
▸ Beklenen çıktı
[ 'fulfilled', 'rejected' ]
server 2
fast
Promise.all failed: no stock
Alıştırma

Hiçbir satırın yerini değiştirmeden çıktının 1, 2, 3, 4 sırasıyla gelmesini sağla: gereken satırları setTimeout(() => ..., 0) ya da Promise.resolve().then(() => ...) ile sar.

Alıştırma · JavaScript
console.log('4. task');
console.log('3. microtask');
console.log('1. sync');
console.log('2. sync');
▸ Beklenen çıktı
1. sync
2. sync
3. microtask
4. task
Alıştırma

Fiyatlar sırayla yükleniyor ve bu yavaş. Kodu, üç isteğin aynı anda başlayacağı şekilde değiştir: Promise.all ve dizi destructuring'i kullan. Yükleme sırası değişecek, toplam ise aynı kalacak.

Alıştırma · JavaScript
const prices = { tea: 2, bread: 1, jam: 5 };

function getPrice(item, ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`loaded ${item}`);
      resolve(prices[item]);
    }, ms);
  });
}

// sequential and slow: rewrite with Promise.all
const tea = await getPrice('tea', 300);
const bread = await getPrice('bread', 100);
const jam = await getPrice('jam', 200);

console.log('Total:', tea + bread + jam);
▸ Beklenen çıktı
loaded bread
loaded jam
loaded tea
Total: 8

Önemli noktalar

  • JavaScript'in tek bir çağrı yığını vardır: aynı anda yalnızca bir kod parçası çalışır, uzun senkron iş her şeyi dondurur.
  • Zamanlayıcılar ve olaylar görev kuyruğuna, Promise callback'leri ve await'ten sonraki kod mikro görev kuyruğuna girer.
  • Sıra: senkron kod → tüm mikro görevler → tek bir görev → yine tüm mikro görevler.
  • Bir async fonksiyonda ilk await'e kadar olan kod senkron çalışır.
  • Bağımsız işleri birlikte başlat ve Promise.all/allSettled ile bekle; race ve any ilk sonucu alır.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); neyi hangi sırayla yazdırır?