JavaScript'in Kalbindeki Orkestra Şefi: Event Loop, Web API'ler ve (Micro)task Kuyruğu

JavaScript'in Kalbindeki Orkestra Şefi: Event Loop, Web API'ler ve (Micro)task Kuyruğu

Merhaba! JavaScript dünyasında gezinirken, muhtemelen "Event Loop" terimiyle karşılaşmışsınızdır. Kulağa karmaşık gelen bu kavram, aslında JavaScript'in kalbindeki ritmi sağlayan bir orkestra şefi gibidir. Ama merak etmeyin, birlikte bu gizemli şefi ve ekibini daha yakından tanıyacağız: Event Loop, Web API'ler ve (Micro)task Kuyruğu.
JavaScript'in tek iş parçacıklı doğası, onu bir tren rayında tek bir lokomotifle seyahat eden bir tren gibi düşünmemizi sağlar. Peki, bu tren nasıl oluyor da aynı anda birden fazla durağa uğruyor, farklı yolcuları alıp bırakıyor ve hiç durmadan yoluna devam ediyor? İşte burada Event Loop ve arkadaşları devreye giriyor!

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

Çağrı Yığını, programımızın yürütülmesini yöneten yer. Bir fonksiyon çağırdığımızda, yeni bir yürütme bağlamı oluşturulur ve bu yığına eklenir. Yığının en üstündeki fonksiyon çalıştırılır ve bu fonksiyon başka fonksiyonları çağırabilir, bu şekilde devam eder.
Bir fonksiyonun yürütmesi tamamlandığında, yürütme bağlamı yığından çıkarılır.
(GIF 2)
JavaScript aynı anda sadece bir görevi işleyebilir; eğer bir görev yığından çıkmak için çok uzun sürüyorsa, diğer görevler işlenemez. Bu, uzun süren görevlerin programımızı dondurabileceği anlamına gelir!
(GIF 3)
Bu durumda, importantTask, longRunningTask yığından çıkana kadar beklemek zorunda kaldı, ki bu da fonksiyonun içindeki yoğun hesaplama nedeniyle uzun sürdü.
Ama bir dakika... Gerçek bir uygulama oluştururken genellikle daha uzun süren görevlere ihtiyaç duyarız. Ağ istekleri, zamanlayıcılar veya kullanıcı girdisine dayalı herhangi bir şey gibi. Peki bu, uzun süren görevler kullandığımızda uygulamamızın tamamen donduğu anlamına mı geliyor?
javascriptKodu kopyalafetch("https://website.com/api/posts") // Verilerin sunucudan ne zaman döneceğini bilmiyoruz... // Bu, veriler dönene kadar çağrı yığında mı kalacak?
Neyse ki hayır! Böyle işlevler aslında JavaScript'in kendisinin bir parçası değildir; bunlar bize Web API'leri aracılığıyla sağlanır.

Web API'leri

Web API'leri, tarayıcının özellikleriyle etkileşim kurmak için bir dizi arayüz sağlar. Bu, JavaScript ile oluştururken sıkça kullandığımız işlevleri içerir: Document Object Model, fetch, setTimeout ve daha fazlası.
(GIF 4)
Tarayıcı, tonlarca özelliği kullanan güçlü bir platformdur. İçerik görüntülemek için Render Motoru veya ağ istekleri için Ağ Yığını gibi işlevler, işlevsel uygulamalar oluşturmamız için gereklidir. Hatta cihazın sensörleri, kameraları, konum bilgisi gibi daha düşük seviye özelliklere bile erişimimiz var.
(GIF 5)
Web API'leri, JavaScript çalışma zamanı ile tarayıcı özellikleri arasında bir köprü görevi görerek, JavaScript'in kendi yeteneklerinin ötesindeki bilgilere ve özelliklere erişmemizi sağlar.
Peki güzel, ama bunun engellemeyen (non-blocking) görevlerle ne ilgisi var?
Bazı Web API'leri, asenkron görevleri başlatmamıza ve daha uzun süren işlemleri tarayıcıya devretmemize izin verir! Bu API'ler tarafından sunulan bir yöntemi çağırmak, aslında daha uzun süren bir görevi tarayıcı ortamına devretmek ve bu görevin sonunda ne yapılacağını belirlemek anlamına gelir.
(GIF 6)
Asenkron görevi başlattıktan sonra (sonucu beklemeden), yürütme bağlamı hızla Çağrı Yığından çıkarılabilir; yani engellemez! Asenkron yetenekler sunan Web API'leri, geri çağırma (callback) tabanlı veya promise tabanlı bir yaklaşım kullanır.
(GIF 7)
Önce geri çağırma yaklaşımından bahsedelim.

Geri Çağırma Tabanlı API'ler

Örneğin Geolocation API'sini ele alalım. Web sitemizde, kullanıcının konumuna erişmek istiyoruz. Bunu almak için, getCurrentPosition yöntemini kullanabiliriz; bu yöntem iki geri çağırma alır: kullanıcının konumunu başarıyla aldığımızda kullanılan successCallback ve bir şeyler ters giderse kullanılan isteğe bağlı errorCallback.
(GIF 8)
Bu fonksiyonu çağırmak, yeni oluşturulan yürütme bağlamını Çağrı Yığına ekler. Bu aslında geri çağırmalarını Web API'sine "kaydetmek" içindir, ardından işlem tarayıcıya devredilir. Fonksiyon daha sonra Çağrı Yığından çıkarılır; artık tarayıcının sorumluluğundadır.
(GIF 9)
Arka planda, tarayıcı kullanıcıya web sitemizin konumlarına erişmesine izin vermesi için bir uyarı gösterir. Kullanıcının uyarı ile ne zaman etkileşime geçeceğini aslında bilmiyoruz; belki dikkatleri dağılır ya da açılan pencereyi görmezler.
Ama bu sorun değil! Tüm bunlar arka planda gerçekleşirken, Çağrı Yığını diğer görevleri alıp yürütmek için müsait kalır. Web sitemiz tepki vermeye ve etkileşimli olmaya devam eder.
(GIF 10)
Sonunda, kullanıcı web sitemizin konumlarına erişmesine izin verdi. API şimdi tarayıcıdan verileri alır ve sonucu işlemek için successCallback'i kullanır.
(GIF 11)
Ancak, successCallback doğrudan Çağrı Yığına eklenemez; çünkü bu, zaten çalışmakta olan bir görevi potansiyel olarak kesintiye uğratabilir, bu da tahmin edilemez davranışlara ve olası çatışmalara yol açabilir.
JavaScript motoru, görevleri birer birer işleyebilir, bu da tahmin edilebilir ve düzenli bir yürütme ortamı sağlar.

Görev Kuyruğu (Task Queue)

Bunun yerine, successCallback Görev Kuyruğuna (tam da bu nedenle Callback Queue da denir) eklenir. Görev Kuyruğu, Web API geri çağırmalarını ve gelecekte bir noktada yürütülmeyi bekleyen olay işleyicilerini tutar.
(GIF 12)
Tamam, şimdi successCallback görev kuyruğunda... Peki ne zaman yürütülecek?

Event Loop (Olay Döngüsü)

İşte şimdi Event Loop devreye giriyor! Event Loop'un sorumluluğu, Çağrı Yığınının boş olup olmadığını sürekli kontrol etmektir. Ne zaman Çağrı Yığını boş olsa — yani şu anda çalışmakta olan bir görev yoksa — Görev Kuyruğundaki ilk mevcut görevi alır ve bunu Çağrı Yığına taşır, burada geri çağırma yürütülür.
(GIF 13)
Event Loop, Çağrı Yığınının boş olup olmadığını sürekli kontrol eder ve eğer boşsa, Görev Kuyruğundaki ilk mevcut görevi alır ve yürütme için Çağrı Yığına taşır.
Bir başka popüler geri çağırma tabanlı Web API'si setTimeout'tur. Ne zaman setTimeout çağırırsak, fonksiyon çağrısı Çağrı Yığına eklenir ve sadece belirtilen gecikmeyle bir zamanlayıcı başlatmaktan sorumludur. Arka planda, tarayıcı zamanlayıcıları takip eder.
(GIF 14)
Bir zamanlayıcı sona erdiğinde, zamanlayıcının geri çağırması Görev Kuyruğuna eklenir! setTimeout'a iletilen gecikmenin, geri çağırmanın Görev Kuyruğuna ne zaman ekleneceğini belirttiğini unutmamak önemlidir, Çağrı Yığına ne zaman ekleneceğini değil.
Bu, gerçek yürütme gecikmesinin setTimeout'a geçirilen belirtilen gecikmeden daha uzun olabileceği anlamına gelir! Eğer Çağrı Yığını hala diğer görevleri işlemekle meşgulse, geri çağırma Görev Kuyruğunda beklemek zorunda kalır.
Şimdiye kadar, geri çağırma tabanlı API'lerin nasıl işlendiğini gördük. Ancak, modern Web API'lerinin çoğu promise tabanlı bir yaklaşım kullanır ve tahmin ettiğiniz gibi, bunlar farklı şekilde ele alınır.

Microtask Kuyruğu

Çoğu (modern) Web API, bir promise döndürür ve bize döndürülen verileri promise zincirleme (ya da await kullanarak) yoluyla ele almamıza izin verir, geri çağırmalar yerine.
javascriptKodu kopyalafetch("...") .then(res => ...) .catch(err => ...)
Verileri bir promise geri çağırmasında ele aldığımız için, Microtask Kuyruğunu kullanıyoruz!
Microtask Kuyruğu, daha yüksek önceliğe sahip başka bir kuyruktur ve özellikle şunlar için ayrılmıştır:
  • Promise geri çağırmaları (
    then(callback)
    ,
    catch(callback)
    , ve
    finally(callback)
    )
  • await
    sonrasındaki async fonksiyon gövdelerinin yürütülmesi
  • MutationObserver
    geri çağırmaları
  • queueMicrotask
    geri çağırmaları
Event Loop, Çağrı Yığını boş olduğunda, önce Microtask Kuyruğundaki tüm mikro görevleri işler, ardından Görev Kuyruğuna geçer.
(GIF 15)
Görev Kuyruğundan tek bir görevi tamamladıktan sonra ve Çağrı Yığını boş olduğunda, Event Loop etkili bir şekilde "baştan başlar" ve önce Microtask Kuyruğundaki tüm mikro görevleri işler, ardından tekrar bir sonraki göreve geçer. Bu, yeni tamamlanan görevle ilgili mikro görevlerin hemen ele alınmasını sağlayarak programın duyarlılığını ve tutarlılığını korur.
(GIF 16)
Popüler bir promise tabanlı API fetch'tir. fetch çağırdığımızda, yürütme bağlamı Çağrı Yığına eklenir.
fetch çağırmak, bellekte varsayılan olarak "pending" olan bir Promise Nesnesi oluşturur. Ağ isteğini başlattıktan sonra, fetch fonksiyon çağrısı Çağrı Yığından çıkarılır.
(GIF 17)
Motor şimdi zincirlenmiş then geri çağırmasını görür, bu da bir PromiseReaction kaydı oluşturur ve PromiseFulfillReactions içinde saklanır.
(GIF 18)
Ardından, console.log Çağrı Yığına eklenir ve "End of script" ifadesini konsola yazar. Bu durumda, ağ isteği hala beklemede.
(GIF 19)
Sunucu sonunda verileri döndürdüğünde, [[PromiseStatus]] "fulfilled" olarak ayarlanır, [[PromiseResult]] ise Response nesnesine ayarlanır. Promise çözüldüğünde, PromiseReaction Microtask Kuyruğuna eklenir.
(GIF 20)
Çağrı Yığını boş olduğunda, Event Loop geri çağırma fonksiyonunu Microtask Kuyruğundan Çağrı Yığına taşır, burada yürütülür, Response nesnesini loglar ve sonunda çağrı yığından çıkarılır.

Tüm Web API'leri asenkron mu işlenir?

Hayır, sadece asenkron işlemleri başlatanlar. Örneğin document.getElementById() veya localStorage.setItem() gibi diğer yöntemler senkron olarak işlenir.

Özet

Şimdiye kadar neleri ele aldığımızı özetleyelim:
  • JavaScript tek iş parçacıklıdır, yani aynı anda sadece bir görevi işleyebilir.
  • Web API'leri, tarayıcı tarafından kullanılan özelliklerle etkileşim kurmak için kullanılır. Bu API'lerin bazıları, daha uzun süren görevleri arka planda başlatmamıza izin verir.
  • Asenkron görevi başlatan fonksiyon çağrısı, Çağrı Yığına eklenir, ancak bu sadece onu tarayıcıya devretmek içindir. Asıl asenkron görev arka planda işlenir ve Çağrı Yığında kalmaz.
  • Görev Kuyruğu, asenkron görev tamamlandıktan sonra geri çağırmaları sıraya almak için geri çağırma tabanlı Web API'leri tarafından kullanılır.
  • Microtask Kuyruğu, Promise geri çağırmaları, await sonrasındaki async fonksiyon gövdeleri, MutationObservergeri çağırmaları ve queueMicrotask geri çağırmaları tarafından kullanılır. Bu kuyruk, Görev Kuyruğundan daha önceliklidir.
  • Çağrı Yığını boş olduğunda, Event Loop önce Microtask Kuyruğundan görevleri taşır ve bu kuyruk tamamen boşalana kadar devam eder. Daha sonra Görev Kuyruğuna geçer ve ilk mevcut görevi Çağrı Yığına taşır. İlk görevi işledikten sonra, tekrar Microtask Kuyruğunu kontrol ederek "baştan başlar".

Callback Tabanlı API'leri Promise'lere Dönüştürmek

Callback tabanlı Web API'lerinde asenkron işlemlerin akışını daha iyi yönetmek ve okunabilirliği artırmak için bunları bir Promise içinde sarmalayabiliriz.
Örneğin, Geolocation API'sinin callback tabanlı getCurrentPosition yöntemini bir Promise yapıcısında sarabiliriz.
javascriptKodu kopyalafunction getCurrentPosition() { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject); }); }
Bu, daha iyi okunabilirlik ve async/await sözdiziminin kullanımı gibi Promise'lerin tam gücünden yararlanmamızı sağlar.
javascriptKodu kopyalaasync function fetchAndLogCurrentPosition() { try { const position = await getCurrentPosition(); console.log(position); } catch (error) { console.log(error); } } // function fetchAndLogCurrentPosition() { // getCurrentPosition() // .then((position) => console.log(position)) // .catch((error) => console.error(error)); // }
Hatta, bir kod bloğunun yürütülmesini bir zamanlayıcı sona erene kadar ertelemek için setTimeout kullanarak Promise tabanlı bir zamanlayıcı oluşturabiliriz:
javascriptKodu kopyalafunction delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } async function doStuff() { // Görevleri gerçekleştir... await delay(5000); // 5 saniye sonra devam et }

Sonuç

JavaScript'in büyülü dünyasında, Event Loop, Görev Kuyruğu ve Microtask Kuyruğunun nasıl birlikte çalıştığını anlamak, asenkron ve engellemeyen JavaScript'i ustalıkla kullanmak için önemlidir.
Event Loop, Microtask Kuyruğuna öncelik vererek görevlerin yürütülmesini düzenler, böylece Promise'ler ve ilgili işlemler, Görev Kuyruğundaki görevlere geçmeden önce hızla çözülür. Bu dinamik, JavaScript'in tek iş parçacıklı bir ortamda karmaşık asenkron davranışı ele almasını sağlar.
Artık JavaScript'in perde arkasında neler olduğunu daha iyi anladığınıza göre, asenkron işlemleri yönetmek sizin için daha anlaşılır ve kontrol edilebilir olacaktır. Umarım bu yazı, konuyu daha iyi kavramanıza yardımcı olmuştur ve gelecekteki projelerinizde size rehberlik eder!