JavaScript Visualized: Promises & Async/Await
Makalenin ingilizce aslı: JavaScript Visualized: Promises & Async/Await
Hiç beklediğiniz gibi çalışmayan bir JS koduyla uğraşmak zorunda kaldınız mı? Belki fonksiyonlar rastgele, öngörülemeyen zamanlarda çalıştırılmış veya execution işlemi gecikmiş gibi görünüyordu. ES6'nın sunduğu harika bir yeni özellikle uğraşma ihtimaliniz var: Promise’lar!
Yıllar öncesindeki merakım karşılığını verdi ve uykusuz gecelerim bir kez daha bana bazı animasyonlar yapma fırsatı tanıdı. Promise hakkında konuşma zamanı: neden kullanmalısınız? ‘arka planda’ nasıl çalışıyorlar ve bunları en modern şekilde nasıl yazabiliriz?
JavaScript Event Loop hakkındaki önceki yazımı henüz okumadıysanız, önce onu okumanız faydalı olabilir! Call Stack, Web API ve queue hakkında bazı temel bilgileri bildiğinizi varsayarak event loop konusunu tekrar ele alacağım, ancak bu sefer bazı heyecan verici ekstra özelliklerini de elden geçireceğiz.
JavaScript Visualized: Event Loop
Giriş
JavaScript yazarken, genellikle başka işlere bağımlı şeylerle uğraşmak zorundayız! Diyelim ki bir görüntü elde etmek, sıkıştırmak, filtre uygulamak ve kaydetmek istiyoruz.
Yapmamız gereken ilk şey, düzenlemek istediğimiz resmi elde etmektir. Bir getImage
fonksiyonu bunu halledebilir! Sadece bu image başarıyla yüklendikten sonra, bu value’yuresizeImage
fonksiyonuna aktarabiliriz. Görüntü başarıyla yeniden boyutlandırıldığında, applyFilter
fonksiyonunda görüntüye bir filtre uygulamak istiyoruz. Resim sıkıştırıldıktan ve bir filtre ekledikten sonra, resmi kaydetmek ve kullanıcıya her şeyin doğru çalıştığını bildirmek istiyoruz!
Bunun sonucunda, böyle bir şeyle karşılaşacağız:
Hmm… Burada bir şey fark ettiniz mi? Her ne kadar … iyi olsa da, harika değil. Önceki callback fonksiyonuna bağlı birçok iç içe geçmiş callback fonksiyonuyla karşılaştık. Kodun okunmasını oldukça zorlaştıran tonlarca iç içe geçmiş callback fonksiyonu elde ettiğimiz için bu genellikle callback hell olarak adlandırılır.
Neyse ki, şimdi bize yardım edecek promise denen bir şeyimiz var! Promise’ların ne olduğuna ve bu gibi durumlarda bize nasıl yardımcı olabileceklerine bir göz atalım!
Promise Syntax
Promise ES6 ile birlikte tanıtıldı. Birçok tutorialda aşağıdaki gibi şeyler okuyacaksınızdır:
‘Bir promise, ilerleyen süreçlerde resolve veya reject edilebilecek olan bir değer için yer tutucudur’
Evet… Bu açıklama benim için hiçbir şeyi açıklığa kavuşturmadı. Aslında bana bir Promise’ın tuhaf, belirsiz, öngörülemez bir sihir olduğunu hissettirdi. O halde promise’ların gerçekte ne olduğuna bakalım.
Callback alan bir Promise constructor’ını kullanarak bir promise oluşturabiliriz. Tamam harika, hadi deneyelim!
Bir dakika? neyi return etti?
Promise
, bir status ([[PromiseStatus]])
ve bir value ([[PromiseValue]])
içeren bir objedir. Yukarıdaki örnekte, [[PromiseStatus]]
değerinin "pending"
olduğunu ve promise value kısmının "undefined”
olduğunu görebilirsiniz.
Endişelenmeyin — bu objeyle asla etkileşime girmenize gerek kalmayacak, [[PromiseStatus]]
ve [[PromiseValue]]
property’lerine bile erişmeniz mümkün değil! Ancak promise’larla çalışırken bu property’lerin değerleri önemlidir.
PromiseStatus
’un (state) değeri şu üç değerden biri olabilir:
✅ fulfilled
: Promise resolve
oldu. Her şey yolunda gitti, promise’da hiçbir hata olmadı.
❌ rejected
: Promise reject
oldu. Bir şeyler ters gitti…
⏳ pending
: Promise ne resolve
oldu ne de reject
edildi (henüz), promise hala beklemede
.
Pekala, bunların hepsi kulağa harika geliyor, ancak bir promise durumu ne zaman ‘pending
’, ‘fulfilled
’ veya ‘rejected
’ olur? Ve bu durumlar neden önemlidir?
Aşağıdaki örnekte, Promise
constructor’ına basit bir callback fonksiyonu () => {}
aktardık. Ancak, bu callback fonksiyonu aslında iki argüman alır. Genellikle resolve
veya res
olarak adlandırılan ilk argümanın değeri, Promise resolve olduğunda çağrılacak metod’dur. reject
veya rej
olarak adlandırılan ikinci argümanın değeri, Promise’in reject olduğu durumda, bir şeyler ters gittiğinde çağrılacak metod’dur.
resolve
veya reject
metodunu çağırdığımızda bunun nasıl log’landığını görelim! Örneğimde, resolve
metodunu res
ve reject
metodunu rej
olarak adlandırdım.
Harika! Sonunda 'pending’
durumundan ve undefined
değerinden nasıl kurtulacağımızı biliyoruz! Bir promise durumu, resolve
metodunu çağırırsak 'fulfilled'
ve rejected
metodunu çağırırsak promise durumu 'rejected'
olur.
Bir promise value [[PromiseValue]]
değeri, argüman olarak resolve
veya rejected
metoduna aktardığımız değerdir.
Pekala, şimdi bu karmaşık Promise
objesini nasıl kontrol edeceğimizi biraz daha iyi biliyoruz. Ama acaba ne için kullanılıyor?
Giriş bölümünde, bir görüntü aldığımız, sıkıştırdığımız, bir filtre uyguladığımız ve kaydettiğimiz bir örnek gösterdim! Sonunda, bu iç içe geçmiş bir callback karmaşasına sebep oldu.
Neyse ki, Promise
bunu düzeltmemize yardımcı olabilir! İlk olarak, tüm kod bloğunu yeniden yazalım, böylece her fonksiyon bunun yerine bir Promise
döndürür.
Görüntü yüklendiyse ve her şey yolunda giderse, promise’ı, yüklenen görüntü ile resolve edelim! Aksi takdirde dosya yüklenirken bir yerde hata oluştuysa oluşan hata ile promise’ı reject edelim.
Bakalım bunu terminalde çalıştırdığımızda ne olacak!
Güzel! Beklediğimiz gibi parse edilen verilerin değeriyle bir promise return edildi.
Ama… şimdi ne olacak? Tüm bu promise objesini umursamıyoruz, sadece verilerin değerini önemsiyoruz! Neyse ki, bir promise değerini elde etmek için built-in metodlar vardır. Bir promise’a 3 metod ekleyebiliriz:
.then()
: Bir promise resolve olduktan sonra çağrılır..catch()
: Bir promise rejected olduktan sonra çağrılır..finaly()
: Promise resolve veya rejected olmuş olsun, her zaman çağrılır.
.then
metodu, resolve
metoduna iletilen değeri alır.
.catch
metodu, rejected
metoduna iletilen değeri alır.
Son olarak, promise objesinin tamamına sahip olmadan promise’ın resolve olan değerine sahibiz! Artık bu değerle istediğimizi yapabiliriz.
Not: bir promise’ın her daim resolve veya rejected olacağını bildiğinizde, promise’ı rejected veya resolve etmek istediğiniz değerle Promise.resolve
veya Promise.reject
yazabilirsiniz!
Bu sözdizimini aşağıdaki örneklerde sıklıkla göreceksiniz.
getImage
örneğinde, örneği çalıştırmak için birden çok callback iç içe geçirmek zorunda kaldık. Neyse ki .then
metodu bu konuda bize yardımcı olabilir!
.then
’in sonucu bir promise değeridir. Bu, istediğimiz kadar .then
zincirleyebileceğimiz anlamına gelir: önceki callback .then
sonucu, bir sonraki .then
callback’e argüman olarak iletilecektir!
getImage
örneğinde, işlenen görüntüyü bir sonraki fonksiyona geçirmek için birden çok .then
callback’i zincirleyebiliriz! Birçok iç içe callback fonksiyonuyla sonuçlandırmak yerine, temiz bir .then
zinciri elde ederiz.
Mükemmel! Bu sözdizimi iç içe geçmiş callback’ten çok daha iyi görünüyor.
Microtasks and (Macro)tasks
Pekala, nasıl bir promise oluşturacağımızı ve bir promise’dan nasıl value alacağımızı biraz daha iyi biliyoruz. Biraz daha kod ekleyelim ve tekrar çalıştıralım:
Nasıl yani?!
İlk olarak Start!
loglandı. Tamam, bunun geleceğini ilk satırda görebilirdik: console.log('Start!')
. Ancak, kaydedilen ikinci value olan End!
, resolve olan promise değeri değil, Ancak End!
konsola log’landıktan sonra, promise değeri log’landı. Peki burada tam olarak neler oluyor?
Sonunda promise’ların gerçek gücünü gördük! JavaScript single thread olsa dahi, bir Promise
kullanarak asenkron davranışlar ekleyebiliriz!
Ama bir saniye, bunu daha önce görmemiş miydik? JavaScript event loop konusunda, bir tür asenkron davranış oluşturmak için setTimeOut
gibi tarayıcıya özgü yöntemleri de kullanamaz mıyız?
Evet! Ancak, Event Loop içinde aslında iki tür queue vardır: (makro)task queue (veya yalnızca task queue olarak adlandırılır) ve mikrotask queue. (Makro)task queue (makro)task’lar içindir ve mikrotask queue mikrotask’lar içindir.
Öyleyse (makro)task nedir ve mikrotask nedir? Burada anlatacağımdan birkaç tane daha olmasına rağmen, en yaygın olanları aşağıdaki tabloda gösterdim.
Evet, mikrotask listesinde Promise
`ı görüyoruz! 😃 Bir Promise
resolve olduğunda ve kendi then()
, catch()
veya finally()
metodunu çağırdığında, metod içindeki callback, mikrotask queue kısmına eklenir! Bu, then()
, catch()
veya finally()
metodundaki callback’in hemen execute edilmediği ve aslında JavaScript kodumuza bazı asenkron davranışlar eklediği anlamına gelir!
Peki then()
, catch()
veya finally()
callback ne zaman çalıştırılır? Event Loop, tasklara farklı bir şekilde öncelik verir:
1- Bu durumda o andaki call stack’te bulunan tüm fonksiyonlar execute edilir. Bir value return ettiklerinde stack’ten çıkarlar.
2- Call stack boşaldığında, sıradaki tüm mikrotask’lar call stack’e birer birer eklenir ve execute edilir! (Mikro task’ların kendileri de yeni mikro tasklar return edebilir ve etkili bir şekilde sonsuz bir mikro task döngüsü oluşturabilir)
3- Hem call stack hem de mikro task queue boşsa, event loop (makro)task queue’da kalan task olup olmadığını kontrol eder. tasklar call stack’e eklenir, execute edilir ve çıkarılır!
Basit bir şekilde aşağıdakileri kullanarak bir örneğe bakalım:
Task1
: Call stack’e anında eklenen bir fonksiyon, örnek olarak kodumuzda anında invoke edildi.
Task2
, Task3
, Task4
: mikro task’lar, örneğin then
callback’i olan bir Promise, veya queueMicrotask
ile eklenen bir task.
Task5
, Task6
: Bir (Makro)task, örneğin callback’i olan bir setTimeOut
veya setImmediate
fonksiyonu.
İlk olarak, Task1
bir value return etti ve call stack’ten çıkarıldı. Ardından, engine mikro task queue’da sıraya giren taskları kontrol etti. Tüm tasklar call stack’e eklendiğinde ve sonunda çıkarıldıktan sonra, engine bu sefer (makro)task queue’daki taskları kontrol eder ve call stack’e ekler, sonunda bunlar da value return ettiklerinde call stack’ten çıkarılırlar.
Tamam, bu kadar pembe kutu yeter. Hadi örneğimizi gerçek bir kodla kullanalım!
Bu kodda, setTimeOut
makro task’ı ve mikro task promise then()
callback metodunu içeriyor. Engine, ilk olarak setTimeOut
fonksiyonunun satırına ulaşacak. Bu kodu adım adım çalıştıralım ve nelerin loglandığını görelim!
Not: aşağıdaki örneklerde, call stack’e
console.log()
,setTimeOut
vePromise.resolve
gibi metodlar eklendiğini gösteriyorum. Bunlar dahili metodlardır ve aslında call stack izlemelerinde görünmezler. Bu yüzden debugger kullanıyorsanız ve bunları hiçbir yerde göremiyorsanız endişelenmeyin! Bir sürü boiler plate kod eklemeden bu kavramı açıklamayı kolaylaştırmak istedim.
İlk satırda, engine console.log()
metoduyla karşılaşır. Callstack’e eklenir ve ardından Start!
konsola basılır. Metod callstack’ten çıkar ve engine devam eder.
Engine, callstack’ten çıkarılan setTimeOut
metoduyla karşılaşır. setTimeOut
metodu tarayıcı için native bir özelliktir: bu yüzden callback fonksiyonu (() => console.log('In timeout')
) (süreyi belirten) zamanlayıcı tamamlanana kadar Web API’sine eklenir. Zamanlayıcı için 0
değerini vermiş olsak da, callback yine de ilk olarak Web API’sine gönderilir, ardından (makro)task queue kısmına eklenir: setTimeOut
bir makro tasktır!
Engine, Promise.resolve()
metoduyla karşılaşır. Promise.resolve()
metodu call stack’e eklenir ve ardından Promise!
value ile resolve edilir. Daha sonra then
callback fonksiyonu mikro task queue kısmına eklenir.
Engine, console.log()
metoduyla karşılaşır. Call stack’e hemen eklenir ve ardından konsola value olarak End!
loglanır, call stack’ten çıkar ve engine devam eder.
Engine call stack’in artık boş olduğunu görür. Call stack boş olduğundan, mikro task queue sıraya alınmış tasklar olup olmadığını kontrol eder! Ve evet var, promise then
callback metodu sırasını bekliyor! Call stack’e girer ve ardından promise resolve değerini loglar: bu durumda string bir Promise!
Engine, call stack’in boş olduğunu görür, bu nedenle tüm taskların queue’ya alınıp alınmadığını görmek için mikro task queue kısmını bir kez daha kontrol eder. Hayır, artık mikro task queue tamamen boş.
Şimdi (makro)task queue kontrol zamanı: setTimeOut
callback hala orada bekliyor! setTimeOut
callback, call stack’e atılır. Callback fonksiyonu, "In timeout!”
string değerini log’layan console.log()
metodunu return eder. setTimeOut
callback, call stack’ten çıkarılır.
Sonunda, hepsi bitti! Görünüşe göre daha önce gördüğümüz çıktı o kadar da beklenmedik değildi.
Async/Await
ES7, JavaScript’e asenkron davranışlar eklemenin ve promise ile çalışmayı kolaylaştırmanın yeni bir yolunu tanıttı! Async
ve await
anahtar sözcüklerinin tanıtılmasıyla, implicitly bir promise
return eden asenkron fonksiyonlar oluşturabiliriz. Ama… acaba bunu nasıl yapabiliriz?
Daha önce, ister new Promise(() => {})
, ister Promise.resolve()
ya da Promise.reject()
yazarak, Promise
objesini explicit olarak oluşturabileceğimizi gördük.
Promise
objesini explicit olarak kullanmak yerine, artık bir objeyi implicitly olarak return eden asenkron işlevler oluşturabiliriz! Bu, artık kendi kendimize herhangi bir Promise
objesi yazmak zorunda olmadığımız anlamına gelir.
Asenkron fonksiyonların implicitly olarak promise return etmesi oldukça büyük bir şey olsa da, async
fonksiyonların gerçek gücü await
anahtar sözcüğünü kullanırken görülebilir! await
anahtar sözcüğü ile, await
ed olan değerin resolve edilmiş bir promise return etmesini beklerken asenkron işlevi askıya alabiliriz. Bu resolve olmuş promise değerini elde etmek istiyorsak, daha önce then()
callback metodunda yaptığımız gibi, değişkenleri await
promise değerine atayabiliriz!
Yani, bir asenkron fonksiyonu askıya alabilir miyiz? Tamam harika ama .. bu ne anlama geliyor?
Aşağıdaki kod bloğunu çalıştırdığımızda ne olacağını görelim:
Hmm… Burada neler oluyor?
İlk olarak, engine bir console.log()
ile karşılaşır. Call stack’e girer ve Before function!
loglandıktan sonra call stack’ten çıkarılır.
Daha sonra, bir asenkron fonksiyon olan myFunc()
çağırırız, ardından myFunc
fonksiyon gövdesi çalışır. Fonksiyon gövdesinin ilk satırında, bu sefer In function
string ifadesiyle başka bir console.log()
çağırıyoruz! console.log()
, call stack’e eklenir, değeri loglar ve stack’ten çıkarılır.
Fonksiyon gövdesi execute edilmeye devam ediyor, bu da bizi ikinci satıra götürüyor. Son olarak bir await
anahtar kelimesi görüyoruz!
İlk gerçekleşen şey, beklenen değerin execute edilmesidir: bu durumda one
fonksiyonudur. Call stack’e girer, resolve olan bir promise retun eder ve call stack’ten çıkarılır. Promise resolve olduğunda ve one
bir value return ettiğinde, engine await
anahtar kelimesiyle karşılaşır.
Bir await
anahtar sözcüğüyle karşılaşıldığında, async
fonksiyon askıya alınır. Fonksiyon gövdesinin execute edilmesi duraklatılır ve asenkron fonksiyonun geri kalanı normal bir task yerine bir mikro task’ta çalışır!
Artık asenkron fonksiyon olan myFunc
, await
anahtar sözcüğüyle karşılaştığında askıya alındığına göre, engine asenkron fonksiyonu atlar ve asenkron fonksiyonun çağrıldığı execution context’inden kodu yürütmeye devam eder: bu durumda, global execution context!
Son olarak, global execution context’te çalıştırılacak başka task yok! Event loop, queue’ya alınmış mikro task olup olmadığını kontrol eder: ve vardır! Asenkron myFunc
fonksiyonu, one
fonksiyonunun değerini resolve ettikten sonra queue’ya alınır. myFunc
, call stack’e geri döner ve daha önce kaldığı yerden çalışmaya devam eder.
res
değişkeni nihayet değerini, yani one
fonksiyonunun return ettiği resolve olmuş promise’ın değerini alır! console.log()
’u res
değeriyle çağırıyoruz: Bu örnekte değeri One!
olan bir string! One!
konsolda loglanır ve call stack’ten çıkar!
Sonunda, hepsi bitti! async
fonksiyonların bir promise then
ile karşılaştırıldığında ne kadar farklı olduğunu fark ettiniz mi? await
anahtar sözcüğü asenkron fonksiyonu askıya alırken, Promise gövdesini then
ile kullansaydık execute edilmeye devam ederdi!
Hmm oldukça fazla bilgi edindik! Promise ile çalışırken hala biraz kafa karışıklığı hissediyorsanız endişelenmeyin, kişisel olarak asenkron JavaScript ile çalışırken pattern’leri fark etmenin ve kendinizden emin hissetmenin deneyim gerektirdiğini hissediyorum.
Ancak, asenkron JavaScript ile çalışırken karşılaşabileceğiniz ‘beklenmedik’ veya ‘öngörülemeyen’ davranışların şimdi biraz daha mantıklı gelmesini umuyorum!
Ve her zaman olduğu gibi, bana her zaman ulaşabilirsiniz.
Promise ve state hakkında daha fazla bilgi edinmek istiyorsanız, aşağıdaki Github reposu bu konudaki farklılıkları açıklamakta mükemmel bir iş çıkarıyor.