Thread Starter
#0
JavaScript Event Loop Detaylı Anlatım
Merhaba arkadaşlar,
Çoğumuz JavaScript ile çalışırken, özellikle asenkron işlemlerle uğraşırken bazı garip veya beklenmedik davranışlarla karşılaşmışızdır. Belki bir kullandığımızda kodumuzun hemen değil, biraz sonra çalıştığını görmüşüzdür. Ya da aynı anda birden fazla uzun süren işlem başlattığımızda, tarayıcının donduğunu veya Node.js uygulamasının yanıt vermeyi kestiğini fark etmişizdir. İşte tüm bu gizemlerin arkasında yatan, JavaScript'in kalbi diyebileceğimiz bir mekanizma var: Event Loop, yani Olay Döngüsü. Bugün bu konuyu enine boyuna, tüm detaylarıyla ele alalım istedim.
JavaScript'in tek iş parçacıklı (single-threaded) bir dil olduğunu hepimiz biliyoruz. Peki bu ne anlama geliyor? Basitçe söylemek gerekirse, JavaScript motoru (V8 gibi), aynı anda sadece tek bir görevi yürütebilir. Yani bir kod bloğu çalışırken, başka hiçbir şey yapılamaz; DOM güncellenemez, kullanıcı etkileşimleri yanıtlanamaz, ağ istekleri işlenemez. Bu durum, senkron kod yazarken pek sorun teşkil etmez. Ancak modern web uygulamalarında veya Node.js backend'lerinde, ağ istekleri, dosya okuma/yazma, zamanlayıcılar gibi uzun sürebilecek işlemlerle sıkça karşılaşıyoruz. Eğer bu işlemler ana iş parçacığını bloke etseydi, uygulamalarımız tamamen donuk ve kullanılamaz hale gelirdi. İşte burada Event Loop devreye giriyor ve JavaScript'in bu tek iş parçacıklı yapısına rağmen nasıl eşzamansız (asynchronous) çalışabildiğini açıklıyor.
Peki, bu sihirli döngü tam olarak hangi bileşenlerle çalışıyor? Temelde üç ana oyuncumuz var: Call Stack (Çağrı Yığını), Web APIs (Tarayıcı API'leri) veya Node.js C++ API'leri ve Callback Queue (Geri Çağırım Kuyruğu). Ve tabii ki, tüm bu orkestrayı yöneten Event Loop'un kendisi.
Call Stack (Çağrı Yığını)
Öncelikle, Call Stack'e bakalım. Burası, JavaScript motorunun o an hangi fonksiyonu çalıştırdığını takip ettiği yer. Tıpkı bir tabak yığını gibi düşünebilirsiniz; en son eklenen (yani çağrılan) fonksiyon en üstte durur ve ilk o işlenir. Bu, LIFO (Last-In, First-Out) prensibiyle çalışır. Bir fonksiyon çağrıldığında yığına eklenir, işi bittiğinde ise yığından çıkarılır. Eğer bir fonksiyon başka bir fonksiyonu çağırırsa, yeni çağrılan fonksiyon onun üzerine eklenir.
Basit bir örnekle anlatayım:
Bu kod çalışırken, Call Stack şu adımları izler:
Web APIs (Tarayıcı API'leri) / Node.js C++ API'leri
JavaScript motoru tek iş parçacıklı olabilir, ama tarayıcılar (veya Node.js çalışma zamanı) değildir. Tarayıcılar, DOM API'leri,, , gibi asenkron işlemler için bize ek yetenekler sunar. Bunlar, JavaScript motorunun bir parçası değil, tarayıcının veya Node.js'in altında çalışan C++ gibi dillerle yazılmış, ayrı iş parçacıklarında çalışan özelliklerdir.
Şöyle düşünün: JavaScript kodu, gibi bir asenkron fonksiyonu çağırdığında, bu fonksiyon Call Stack'ten çıkarılır ve ilgili Web API'ye devredilir. Yani JavaScript motoru bu işlemi kendi yapmaz, işi başkasına verir ve kendi yoluna devam eder. Bu sayede ana iş parçacığı bloke olmaz. Örneğin, çağrıldığında, fonksiyonu belirli bir süresi boyunca Web API'lerinde bekletilir.
Callback Queue (Geri Çağırım Kuyruğu) / Task Queue (Görev Kuyruğu)
Web API'lerinde bekleyen asenkron işlemler tamamlandığında (örneğin,'un süresi dolduğunda veya bir ağ isteği yanıt döndüğünde), bu işlemlerin ilişkili geri çağırım fonksiyonları (callback'leri) hemen Call Stack'e geri dönemez. Neden mi? Çünkü Call Stack meşgul olabilir! İşte bu noktada Callback Queue devreye girer. Tamamlanan asenkron işlemlerin callback'leri, Call Stack boşalana kadar bu kuyrukta bekler. Bu kuyruk, FIFO (First-In, First-Out) prensibiyle çalışır; yani ilk giren callback, ilk işlenmeyi bekler.
Bu kuyruğa "Macrotask Queue" (Makrogörev Kuyruğu) da denir. DOM olayları,, , (bazı durumlarda), I/O işlemleri gibi görevler bu kuyruğa girer.
Event Loop (Olay Döngüsü)
Ve işte sahneye Event Loop çıkıyor! Event Loop, sürekli olarak iki şeye bakar:
Bu döngü sayesinde, JavaScript tek iş parçacıklı olmasına rağmen eşzamanlı gibi bir deneyim sunar. Uzun süren bir ağ isteği gönderdiğinizde, JavaScript motoru o isteğin bitmesini beklemez; Call Stack'ten çıkarır, Web API'sine devreder ve kendi senkron kodunu çalıştırmaya devam eder. Ağ isteği tamamlandığında, callback'i Callback Queue'ya eklenir ve Event Loop, Call Stack boşaldığında onu işler.
Microtask Queue (Mikrogörev Kuyruğu)
Şimdi işler biraz daha ilginçleşiyor. Event Loop'un sadece Callback Queue'ya değil, bir de Microtask Queue'ya baktığını öğrenmemiz gerekiyor. Mikrogörevler, makrogörevlerden (Callback Queue'daki görevler) daha yüksek önceliğe sahiptir. Yani Event Loop, bir makrogörevi alıp Call Stack'e itmeden önce, Microtask Queue'da bekleyen tüm mikrogörevleri bitirir.
Peki neler mikrogörevdir? En bilinenleri:
Event Loop çalışma prensibi şu şekilde güncelleniyor:
'dan bile önce çalıştığını açıklar.
Bir örnekle bu durumu netleştirelim:
Bu kodun çıktısı ne olur dersiniz?
Gördüğünüz gibi, bile Promise'lerden sonra çalıştı, çünkü Promise callback'leri mikrogörev olarak daha yüksek önceliğe sahip.
Render Kuyruğu (Tarayıcıda)
Tarayıcı ortamında, Event Loop'un döngüsünde bir de render (ekran çizimi) aşaması vardır. Genellikle, bir makrogörev tamamlandıktan ve tüm mikrogörevler işlendikten sonra, tarayıcı bir sonraki kareyi çizmeden önce DOM değişikliklerini uygular ve ekranı günceller. Bu, gibi API'lerin neden genellikle DOM manipülasyonları için tercih edildiğini açıklar; çünkü tarayıcının render döngüsüyle senkronize çalışır ve "jank" (takılma) oluşmasını engellemeye yardımcı olur.
Özetle ve Pratikte Anlamı
Event Loop'u anlamak, özellikle performanslı ve yanıt verebilir uygulamalar geliştirmek için kritik öneme sahip.
Peki siz Event Loop ile ilgili hangi senaryolarda zorlandınız veya hangi detaylar sizi şaşırttı? Belki de gözden kaçırdığım, sizin eklemek istediğiniz önemli noktalar vardır... Deneyimlerinizi ve görüşlerinizi merak ediyorum!
Merhaba arkadaşlar,
Çoğumuz JavaScript ile çalışırken, özellikle asenkron işlemlerle uğraşırken bazı garip veya beklenmedik davranışlarla karşılaşmışızdır. Belki bir
CODE
1setTimeout(fn, 0)JavaScript'in tek iş parçacıklı (single-threaded) bir dil olduğunu hepimiz biliyoruz. Peki bu ne anlama geliyor? Basitçe söylemek gerekirse, JavaScript motoru (V8 gibi), aynı anda sadece tek bir görevi yürütebilir. Yani bir kod bloğu çalışırken, başka hiçbir şey yapılamaz; DOM güncellenemez, kullanıcı etkileşimleri yanıtlanamaz, ağ istekleri işlenemez. Bu durum, senkron kod yazarken pek sorun teşkil etmez. Ancak modern web uygulamalarında veya Node.js backend'lerinde, ağ istekleri, dosya okuma/yazma, zamanlayıcılar gibi uzun sürebilecek işlemlerle sıkça karşılaşıyoruz. Eğer bu işlemler ana iş parçacığını bloke etseydi, uygulamalarımız tamamen donuk ve kullanılamaz hale gelirdi. İşte burada Event Loop devreye giriyor ve JavaScript'in bu tek iş parçacıklı yapısına rağmen nasıl eşzamansız (asynchronous) çalışabildiğini açıklıyor.
Peki, bu sihirli döngü tam olarak hangi bileşenlerle çalışıyor? Temelde üç ana oyuncumuz var: Call Stack (Çağrı Yığını), Web APIs (Tarayıcı API'leri) veya Node.js C++ API'leri ve Callback Queue (Geri Çağırım Kuyruğu). Ve tabii ki, tüm bu orkestrayı yöneten Event Loop'un kendisi.
Call Stack (Çağrı Yığını)
Öncelikle, Call Stack'e bakalım. Burası, JavaScript motorunun o an hangi fonksiyonu çalıştırdığını takip ettiği yer. Tıpkı bir tabak yığını gibi düşünebilirsiniz; en son eklenen (yani çağrılan) fonksiyon en üstte durur ve ilk o işlenir. Bu, LIFO (Last-In, First-Out) prensibiyle çalışır. Bir fonksiyon çağrıldığında yığına eklenir, işi bittiğinde ise yığından çıkarılır. Eğer bir fonksiyon başka bir fonksiyonu çağırırsa, yeni çağrılan fonksiyon onun üzerine eklenir.
Basit bir örnekle anlatayım:
CODE
12345678910111213
function ikinciIslem() {
console.log("İkinci işlem tamamlandı.");
}
function birinciIslem() {
console.log("Birinci işlem başladı.");
ikinciIslem(); // ikinciIslem çağrılıyor, yığına ekleniyor
console.log("Birinci işlem bitti.");
}
birinciIslem(); // birinciIslem çağrılıyor, yığına ekleniyor
console.log("Tüm kod bitti.");
Bu kod çalışırken, Call Stack şu adımları izler:
- Global execution context yığına eklenir.
- yığına eklenir.CODE
1
birinciIslem()
- çalışır.CODE
1
console.log("Birinci işlem başladı.")
- yığına eklenir.CODE
1
ikinciIslem()
- çalışır.CODE
1
console.log("İkinci işlem tamamlandı.")
- biter, yığından çıkarılır.CODE
1
ikinciIslem()
- çalışır.CODE
1
console.log("Birinci işlem bitti.")
- biter, yığından çıkarılır.CODE
1
birinciIslem()
- çalışır.CODE
1
console.log("Tüm kod bitti.")
- Global execution context biter, yığından çıkarılır.
Web APIs (Tarayıcı API'leri) / Node.js C++ API'leri
JavaScript motoru tek iş parçacıklı olabilir, ama tarayıcılar (veya Node.js çalışma zamanı) değildir. Tarayıcılar, DOM API'leri,
CODE
1setTimeout()CODE
1fetch()CODE
1XMLHttpRequest()Şöyle düşünün: JavaScript kodu,
CODE
1setTimeout()CODE
1setTimeout(callback, delay)CODE
1callbackCODE
1delayCallback Queue (Geri Çağırım Kuyruğu) / Task Queue (Görev Kuyruğu)
Web API'lerinde bekleyen asenkron işlemler tamamlandığında (örneğin,
CODE
1setTimeoutBu kuyruğa "Macrotask Queue" (Makrogörev Kuyruğu) da denir. DOM olayları,
CODE
1setTimeoutCODE
1setIntervalCODE
1requestAnimationFrameEvent Loop (Olay Döngüsü)
Ve işte sahneye Event Loop çıkıyor! Event Loop, sürekli olarak iki şeye bakar:
- Call Stack boş mu?
- Callback Queue'da bekleyen bir görev var mı?
Bu döngü sayesinde, JavaScript tek iş parçacıklı olmasına rağmen eşzamanlı gibi bir deneyim sunar. Uzun süren bir ağ isteği gönderdiğinizde, JavaScript motoru o isteğin bitmesini beklemez; Call Stack'ten çıkarır, Web API'sine devreder ve kendi senkron kodunu çalıştırmaya devam eder. Ağ isteği tamamlandığında, callback'i Callback Queue'ya eklenir ve Event Loop, Call Stack boşaldığında onu işler.
Microtask Queue (Mikrogörev Kuyruğu)
Şimdi işler biraz daha ilginçleşiyor. Event Loop'un sadece Callback Queue'ya değil, bir de Microtask Queue'ya baktığını öğrenmemiz gerekiyor. Mikrogörevler, makrogörevlerden (Callback Queue'daki görevler) daha yüksek önceliğe sahiptir. Yani Event Loop, bir makrogörevi alıp Call Stack'e itmeden önce, Microtask Queue'da bekleyen tüm mikrogörevleri bitirir.
Peki neler mikrogörevdir? En bilinenleri:
- Promise callback'leri (,CODE
1
.then(),CODE1
.catch())CODE1
.finally()
- ile eklenen görevlerCODE
1
queueMicrotask()
- callback'leri (DOM değişikliklerini izleyen API)CODE
1
MutationObserver
- (Node.js'e özgü, Promise'lerden bile önce çalışır)CODE
1
process.nextTick()
Event Loop çalışma prensibi şu şekilde güncelleniyor:
- Call Stack'te hiçbir şey yok mu?
- O zaman Microtask Queue'daki tüm görevleri Call Stack'e sırayla it ve çalıştır.
- Microtask Queue da boşaldı mı?
- Şimdi Callback Queue'daki ilk görevi al ve Call Stack'e it.
- Bu döngü sürekli tekrar eder.
CODE
1setTimeout(fn, 0)Bir örnekle bu durumu netleştirelim:
CODE
1234567891011121314
console.log("Senkron 1");
setTimeout(() => {
console.log("setTimeout callback"); // Macrotask
}, 0);
Promise.resolve().then(() => {
console.log("Promise then 1"); // Microtask
}).then(() => {
console.log("Promise then 2"); // Microtask
});
console.log("Senkron 2");
Bu kodun çıktısı ne olur dersiniz?
- Önce "Senkron 1" yazdırılır. Call Stack'te.
- çağrılır, Web API'lerine gider. Callback'i (console.log("setTimeout callback")) belirli bir süre sonra (0ms bile olsa) Macrotask Queue'ya eklenmeyi bekler.CODE
1
setTimeout
- ile bir Promise oluşturulur.CODE
1
Promise.resolve()callback'i (console.log("Promise then 1")) Microtask Queue'ya eklenir.CODE1
.then()
- yazdırılır. Call Stack'te.CODE
1
console.log("Senkron 2")
- Call Stack boşalır.
- Event Loop, önce Microtask Queue'ya bakar. "Promise then 1" callback'ini alır, Call Stack'e iter. "Promise then 1" yazdırılır.
- Bu Promise'in ikinci callback'i (console.log("Promise then 2")) de Microtask Queue'ya eklenir (çünkü ilk then bitti).CODE
1
.then()
- Microtask Queue'da başka bir görev var mı? Evet, "Promise then 2". Onu da alır, Call Stack'e iter. "Promise then 2" yazdırılır.
- Microtask Queue boşalır.
- Şimdi Event Loop, Macrotask Queue'ya bakar. "setTimeout callback"i alır, Call Stack'e iter. "setTimeout callback" yazdırılır.
CODE
123456
Senkron 1
Senkron 2
Promise then 1
Promise then 2
setTimeout callback
Gördüğünüz gibi,
CODE
1setTimeout(fn, 0)Render Kuyruğu (Tarayıcıda)
Tarayıcı ortamında, Event Loop'un döngüsünde bir de render (ekran çizimi) aşaması vardır. Genellikle, bir makrogörev tamamlandıktan ve tüm mikrogörevler işlendikten sonra, tarayıcı bir sonraki kareyi çizmeden önce DOM değişikliklerini uygular ve ekranı günceller. Bu,
CODE
1requestAnimationFrameÖzetle ve Pratikte Anlamı
Event Loop'u anlamak, özellikle performanslı ve yanıt verebilir uygulamalar geliştirmek için kritik öneme sahip.
- Asla Call Stack'i uzun süreli, bloklayıcı senkron işlemlerle meşgul etmeyin. Eğer yaparsanız, uygulamanız donar.
- Asenkron işlemleri (,CODE
1
fetch,CODE1
setTimeouttabanlı işlemler) kullanarak ana iş parçacığını serbest bırakın.CODE1
Promise
- Promise'lerin ve 'ınCODE
1
queueMicrotaskgibi diğer asenkron API'lere göre daha yüksek önceliğe sahip olduğunu unutmayın. Bu, özellikle karmaşık zamanlama senaryolarında önemlidir.CODE1
setTimeout
- Node.js'te mikrogörevlerden bile daha yüksek önceliklidir ve mevcut Call Stack'teki kod bittikten hemen sonra çalışır, Event Loop'a geçmeden önce. Bu, Node.js'e özgü bir detaydır.CODE
1
process.nextTick
Peki siz Event Loop ile ilgili hangi senaryolarda zorlandınız veya hangi detaylar sizi şaşırttı? Belki de gözden kaçırdığım, sizin eklemek istediğiniz önemli noktalar vardır... Deneyimlerinizi ve görüşlerinizi merak ediyorum!