[Çeviri] JavaScript Motorları: Nasıl Çalışıyorlar?
Call Stack’ten Promise. Neredeyse Bilmeniz Gereken Her şey
Bu makale Valentino Gagliardi tarafından yazılan JavaScript Engines: How Do They Even Work? From Call Stack to Promise, (almost) Everything You Need to Know makalesinin Türkçe çevirisidir. Makaleyi orijinal dilinde okumak için aşağıdaki linki tıklayabilirsiniz.
Çeviri yaparken JavaScript jargonunda yerleşmiş bazı kavramları Türkçe’ye çevirmedim ve bu kavramları özel isimmiş gibi kabul edip ona göre kullandım. Örneğin
callback hell
kavramını,Callback hell
olarak ele aldım. Özel isimlere uygulanan dil bilgisi yapısını uyguladım. kKesme işareti kullanımı vb. Global Memory, Event Loop gibi kavramlarda da benzer süreci işlettim
Başlıyoruz…
Call Stack, Global Memory, Event Loop, Callback Quee’den Promises ve Async/Await de kadar, JavaScript motorlarında kasırgalı bir yolculuk. İyi okumalar!
Tarayıcıların JavaScript kodunu nasıl okuduğunu ve çalıştırdığını hiç merak ettiniz mi? Sihir gibi görünebilir ama Motor’un içerisinde nelerin olduğunu az çok tahmin edebilirsiniz.
JavaScript motorların harika dünyasını açıklayarak, konumuzda derinlere dalmaya başlayalım.
Chrome tarayıcınızda, konsolda Source
sekmesine bakınız. Call Stack isimli ilginç bir kutucuğun da bulunduğu bazı kutucuklar göreceksiniz.(Firefox’ta code break point koyduktan sonra Call Stack bilgisini görebilirsiniz):
Call Stak nedir? Bir kaç satır kodu çalıştırmak için bir çok şey var görünüyor.
JavaScript’te kodumuzu derleyen ve çalıştıran büyük bir bileşen vardır: Bu bileşene JavaScript motoru diyoruz. Popüler JavaScript motorları; Google Chrome ve Node.js tarafından kullanılan V8, Firefox için SpiderMonkey ve Safari/Webkit tarafından kullanılan JavaScriptCore’dur.
Günümüzde JavaScript motorları mükemmel mühendislik örneğidir. Ve her bir parçasından bahsetmek imkansızdır. Ama her motorun bizim için çok çalışan bazı bileşenleri vardır.
Bu bileşenler; Global Memory ve Execution Context’le birlike yazdığımız kodun çalışmasını sağlayan Call Stack’tır. Bunlarla tanışmaya hazır mısınız?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
İçerik 1.JavaScript motorları ve Global Memory 2.JavaScript Motorları: Nasıl çalışıyorlar? Global Execution Context ve Call Stack 3.JavaScripr single-threaded’tir ve diğer komik hikayeler 4.Asynchronous JavaScript, Callback Queue ve the Event Loop 5.Callback hell ve ES6 Promises 6.JavaScript Promiseler oluşturmak ve çalışmak 7.ES6 Promiselerde hata işlemleri 8.ES6 Promises combinators: Promise.all, Promise.allSettled, Promise.any, and friends 9.ES6 Promiseler ve microtask queue 10.JavaScript Motorları: Nasıl çalışıyorlar? Eş zamansız(asynchronous) evrim: Promiselerden async/await’e 11.JavaScript Motorları: Nasıl çalışıyorlar? Kapanış |
JavaScript Motorları ve Global Memory
Belirttiğim gibi; JavaScript aynı anda derlenen ve yorumlanan bir dildir. İster inanın inanmayın, JavaScript motorları; kodu çalıştırmadan bir kaç mikro saniye öncesinde derliyor.
Sihir gibi değil mi? Bu sihire JIT (Just in time compilation) diyoruz. JIT’in kendisi çok büyük bir konudur, bir kitap bile JIT’in nasıl çalıştığını anlatmaya yetmeyebilir. Şimdilik derlemenin arkasında yatan teori kısmını geçiyor ve yine ilginç olan kodun çalışma aşamasına (the execution phase)odaklanıyoruz.
Başlamak için aşağıdaki kodu ele alalım:
1 2 3 4 5 6 7 8 9 |
var num = 2; function pow(num) { return num * num; } |
“Yukarıdaki kod tarayıcıda hangi süreçlerden geçiyor?” diye sorsaydım ne söylerdiniz? “Tarayıcı kodu okur” veya “Tarayıcı kodu çalıştırır” diye bilirsiniz.
Gerçek durum bundan biraz daha farklı. Öncelikle, tarayıcı bu kod parçacığına bir şey yapmaz. Bu işlemi yapan motordur. JavaScript motoru kodu okur ve en kısa sürede, ilk satırla karşılaşır karşılaşmaz, Global Memory’e bir kaç referans koyar.
Global Memory (Heap de diyebiliriz); JavaScript motorunda, değişkenlerin ve fonksiyon tanımlarının tutulduğu alandır. Örneğimize dönersek, Motor üsteki kodu okuduktan sonra Global Memory iki bağlama(binding) ile doldurulur.
Kodunuzda sadece bir değişken ve bir fonksiyon olsa bile; Tarayıcı veya Node.js’te, JavaScript kodu daha büyük bir ortamda(dünyada) çalışır. Bu ortamda daha önce tanımlanmış ve global diye adlandırdığımız, bir sürü hazır fonksiyon ve değişkenler bulunmaktadır. Global Memory, num
ve pow
’dan daha gazla bilgi tutacağını unutmayın.
Bu noktada kodumuzda çalışan herhangi bir şey yok. Şimdi fonksiyonumuz çalıştırmayı denersek:
1 2 3 4 5 6 7 8 9 10 11 |
var num = 2; function pow(num) { return num * num; } pow(num); |
Ne olacak? İşler ilginçleşmeye başlıyor. Bir fonksiyon çalıştırıldığında, JavaScript motoru iki kutuya daha yer açar.
- Global Execution Context
- Call Stack
Bu kavramların ne olduklarını bir sonraki bölümde görelim.
JavaScript Motorları: Nasıl Çalışıyorlar? Global Execution Context ve Call Stack
Değişkenlerin ve fonksiyon tanımlarının, Javascrit motoru tarafından nasıl okunduğunu öğrendiniz. Hepsi Global Memory (Heap)de tutulur.
Ama şimdi bir adet JavaScript fonksiyonu çalıştırdık ve motor’un bu durumla ilgilenmesi gerekiyor. Nasıl? JavaScript’te Call Stack diye adlandırılan bir ana bileşen mevcuttur.
Call Stack bir yığın veri yapısıdır. Bunun anlamı; elementler yığına üsten girebilir ve elementin üzerinde başka bir element varsa yığından çıkamazlar (LIFO). JavaScript fonksiyonları tam olarak böyle bir şeydir.
Fonksiyonlar çalıştırıldıktan sonra, işlemi devam eden başka bir fonksiyon varsa, Call Stack’ı terk edemezler. “JavaScript single-threaded” kavramının aklınızda yer etmesi açısından, burası önemlidir.
Ama şimdi örneğimize geri dönelim. Fonksiyon çağrıldığında; motor fonksiyonu Call Stack’in içine ekliyor.
Call Stack’i; Pringles yığını olarak düşünebilirsiniz. En alttaki cipsi yemek için, ilk olarak üsteki tüm cipsleri yemeniz gerekiyor. Şanslıyız ki; basit bir çarpma işlemini hızlıca yapan fonksiyonumuz eş zamanlıdır(synchronous).
Hemen hemen aynı zamanda motor, Global ortamda bulunan ve Javascript kodunun çalıştırıldığı, Global Execution Context’ini ayırır. İlgili alan şu şekilde görünür.
Global Execution Context’i , JavaScript’e ait global fonksiyonların yüzdüğü bir deniz gibi düşünebilirsiniz. Ne hoş! Ama bu sadece hikayenin bir yarısı. Eğer fonksiyonumuz iç içe değişkenler veya birden çok iç fonksiyon içerseydi o zaman ne olurdu?
Aşağıdaki gibi küçük bir varyasyonda bile, JavaScript motoru Local Execution Context’i yaratır.
1 2 3 4 5 6 7 8 9 10 11 12 |
var num = 2; function pow(num) { var fixed = 89; return num * num; } pow(num); |
Dikkat ederseniz; pow fonksiyonunun içine fixed isminde bir değişken ekledim. Bu durumda; fixed değişkenin tutulması için Local Execution Context bir kutu oluşturur.
İç içe küçük kutucuklar çizme konusunda iyi değilimdir. Bunu sizin hayal gücünüze bırakıyorum.
Local Execution Context hemen pow
’un yanında, Global Execution Context’in içerisinde yeşilimsi bir kutu olarak görünecektir. Her bir iç içe fonksiyon içinde, iç içe fonksiyonlar için, motor daha fazla Execution Contextsyaratıldığını hayal edin. Bu kutucuklar çok hızlı bir şekilde görünüp kaybolabilir. Matruşka Bebek gibi!
Single-threaded hikayemize dönmeye ne dersiniz. Ne anlama gelmektedir?
JavaScript single-threaded’tır ve diğer komik hikayeler
Fonksiyonlarımızı ele alan bir tane Call Stack olduğundan, JavaScript single-threaded’tır diyebiliriz. Call Stack’ta çalıştırılmayan fonksiyonlar varsa, çalıştırılmasını beklediğimiz fonksiyon Call Stack’ten ayrılamaz.
Eş zamanlı(synchronous) kodlarla işlem yaparken bu bir sorun değildir. Örneğin; iki sayıyı toplayan toplama fonksiyonu eş zamanlı(synchronous) olarak mikro saniyeler içinde çalışır. Ama network çağrıları ve dış dünya ile etkileşim olduğunda ne olur?
Şansımıza ki JavaScript motorları; varsayılan olarak eş zamansız (asynchronous) olacak şekilde tasarlanmışlardır. Her ne kadar, her seferinde bir fonksiyon çalıştırsalar bile, fonksiyonu yavaşlatan harici durumlar vardır. Senaryomuzda tarayıcı. Bunu daha sonra açıklayacağız.
Bu arada; Tarayıcının, JavaScript kodu motor tarafından okunurken bazı şeyleri yüklediğini ve şu adımları takip ettiğini öğrenmiştiniz:
- Global Memory (Heap)’ye değişkenler ve fonksiyon tanımları yüklenir.
- Tüm fonksiyon çağrıları Call Stack’e itilir(push).
- Global fonksiyonların çalıştırıldığı Global Execution Context yaratılır.
- Bir sürü küçük Local Execution Context’ler yaratılır(İç içe değişkenler ve fonksiyonlar varsa).
Her JavaScript motorunun altında yatan, mekanizmaya ait büyük resmişimdiye kadar görmüş olmalısınız. Bir sonraki bölümde eş zamansız(asynchronous) kodların Javascript’te nasıl çalıştığını ve niye böyle bir yolu seçtiğini göreceksiniz.
Asynchronous JavaScript, Callback Queue ve Event Loop
Global Memory, Execution Context ve Call Stack, eş zamanlı(synchronous) JavaScript kodunun tarayıcılarımızda nasıl çalıştığını açıkladık. Yine de bir şeyler eksik. Asynchronous JavaScript kodu çalıştığı zaman ne oluyor?
Asynchronous fonksiyon derken, tamamlanması zaman alan dış sistemlerle ve olan her bir etkileşimden bahsediyorum. REST API çağırmak veya çalışması bir kaç saniye süren bir timer
çağırmak. Call Stack’i engellemeyen işlemleri, motorun ve tarayıcının sahip olduğu bileşenlerle halletmesinin bir yolu yok.
Unutmayın Call Stack aynı anda sadece bir fonksiyon çağırır. Hatta bir uzun süren bloklayan bir fonksiyon, tam anlamıyla tarayıcıyı dondurabilir. Şanslıyız ki Javascript motorları çok zeki ve bu tarz işlemleri tarayıcıdan ufak bir yardım alarak çözebilir.
Asynchronous fonksiyon çalıştırdığımız zaman, tarayıcı fonksiyonu alır ve bizim yerimize çalıştırır. Aşağıdaki timer
’i ele alalım.
1 2 3 4 5 6 7 8 9 |
setTimeout(callback, 10000); function callback(){ console.log('hello timer!'); } |
setTimeout fonksiyonu defalarca gördüğünüzden eminim. Bu fonksiyonun JavaScript’te ait olmadığını bilemeyebilirsiniz. JavaScript doğduğunda setTimeout fonksiyonu dilin içine eklenmemiş.
setTimeout fonksiyonu aslında; bize kullanmamız için tarayıcılar tarafından ücretsiz verilen, yararlı bir araç olan Browser APIs’nin bir parçasıdır. Ne hoş! Pratikte ne demektir bu? setTimeout doğrudan tarayıcı tarafından çalıştırılan bir Browser APIs olduğundan, Call Stack’te bir anlığına görünür ve anında kaybolur.
On saniye sonra tarayıcı, verdiğimiz callback
fonksiyonunu alır ve Callback Queue’ye iletir. Bu noktada JavaScript motorumuzda iki tane daha kutumuz oldu. Aşağıdaki kodu ele alırsanız:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var num = 2; function pow(num) { return num * num; } pow(num); setTimeout(callback, 10000); function callback(){ console.log('hello timer!'); } |
Çizimimizi aşağıdaki şekilde tamamlarız:
Gördüğünüz gibi, setTimeout tarayıcı içeriğinde(context) çalışır. On saniye sonra timer
tetiklenir ve callback
fonksiyonu çalışmaya hazırdır. Ama önce Callback Queue’ye gitmesi gerekmektedir. Callback Queue kuyruk veri yapısıdır ve isminden de anlaşılacağı gibi fonksiyonların sıralı bir listesidir.
Her eş zamansız (asynchronous) fonksiyonun, Call Stack’e iletilmeden önce Callback Queue’ye geçmesi gerekmektedir. Peki kim bu fonksiyonları ileri iten? Event Loop adında bir bileşen(component) daha var.
Event Loop’ın simdilik sadece bir görevi var: Call Stack’in boş olup olmadığını kontrol etmek. Callback Queue bekleyen fonksiyon(lar) varsa ve Call Stack boşsa; callback
fonksiyonunun Call Stack’a gönderilme zamanı gelmiştir.
Bu işlem yapıldıktan sonra fonksiyon çalıştırılır. JavaScript motorunun asynchronous ve synchronous kodu ile nasıl başa çıktığını gösteren büyük bir resim aşağıda mevcuttur:
callback()
’in çalıştırılmaya hazır olduğunu hayal edin. pow() fonksiyonu çalıştırıldıktan sonra Call Stack boşalıyor ve Event Loop callback()
fonksiyonunu gönderir. İşte bu. Ne kadar basitleştirsem bile, eğer yukarıdaki görseli anladıysanız, tüm JavaScript’ti anlamaya hazırsınız.
Browser APIs, Callback Queue, ve Event Loop’un eş zamansız (asynchronous) JavaScript’in temel yapı taşları olduğunu unutmayın.
Eğer video izlemekten keyif alıyorsanız; Philip Roberts tarafından hazırlanan “ What the heck is the event loop anyway” videosunu tavsiye ederim. Event Loop için yapılmış en iyi açıklamalardan birisidir.https://www.youtube.com/watch?v=8aGhZQkoFbQ
Durun eş zamansız (asynchronous) JavaScript ile ilgili henüz bir şey yapmadık. Bir sonraki bölümde ES6 Promise’lere daha yakında bakacağız.
Callback hell and ES6 Promises
Callback fonksiyonları JavaScript’te her yerdedir. Eş zamanlı (synchronous) ve Eş zamansız (asynchronous) kodlarda kullanılır. Aşağıdaki map metodunu ele alalım:
1 2 3 4 5 6 7 8 9 |
function mapper(element){ return element * 2; } [1, 2, 3, 4, 5].map(mapper); |
mapper
fonksiyonu, map’e geçirilen callback fonksiyondur. Üsteki kodu eş zamanlı (synchronous )JavaScript koddur. Ama bunun yerine interval’ı ele alalım.
1 2 3 4 5 6 7 8 9 |
function runMeEvery(){ console.log('Ran!'); } setInterval(runMeEvery, 5000); |
Yukarıdaki kod eş zamansızdır (asynchronous). Yine de runMeEvery
callback fonksiyonunu setInterval
fonksiyonuna geçirildiğini görebilirsiniz. Callback’ler JavaScript’tin içinde yaygın olarak kullanılır. Bu yüzden yıllar içinde bir problem ortaya çıkmıştır. Bu problemin adı Callback hell.
JavaScript’teki Calllback hell
, iç içe callbacklerin içinde, iç içe callbacklerin geçtiği bir programlama stilidir. JavaScript’tin eş zamansız (asynchronous) doğası gereği, bir çok yazılımcı yıllarca kendini kapana sıkışmış gibi hissetmiştir.
Dürüst olmak gerekirse, Her zaman bağlı kalmaya çalıştığım okunabilir kod ilkesinden ötürü, aşırı büyük callback piramitlerinde hiç çalışmadım. Eğer siz de Callback hell’in içindeyseniz, bu fonksiyonunuzun bir çok iş yaptığının bir işaretidir.
Burada Callback hell’den bahsetmeyeceğim. Eğer bu konuyu merak ediyorsanız callbackhell.com sitesine bakabilirsiniz. Bizim odaklanmak istediğimiz ES6 Promiseler. ES6 Promiseler; Callback hell’i çözmeyi amaçlayan JavaScript eklentisidir. İyi de nedir bu Promise? JavaScript Promise, gelecekteki bir olayın temsil edilmesidir. Bir Promise başarılı bitebilir. Biz buna resolved (fulfilled) diyoruz. Eğer bir Promise hatalı bitmiş ise, buna rejected durum diyoruz. Ayrıca Promise’nin varsayılan bir durumu var. Yeni bir Promise yaratıldığında varsayılan olarak pending durumu başlar. Kendi Promisemizi yaratmak mümkün müdür? Evet. Bir sonraki bölümde nasıl yapıldığına bakalım.
JavaScript Promiseler yaratmak ve çalışmak
Yeni bir Promise yaratmak için; Promise constructor’una callback fonksiyonu geçip çalıştırmak yeterlidir. Callback fonksiyonu resolve ve reject şeklinde iki parametre alabilir. Beş saniye sonra tamamlanan bir Promise oluşturalım(Konsole bu örneği sizde denebilirsiniz.)
1 2 3 4 5 6 7 8 9 |
const myPromise = new Promise(function(resolve){ setTimeout(function(){ resolve() }, 5000) }); |
Gördüğünüz gibi; resolve , Promise’nin başarılı bir şekilde tamamlanması için çağırdığımız fonksiyondur. Diğer taraftan Promise’yi ret etmek için reject kullanılır.
1 2 3 4 5 6 7 8 9 |
const myPromise = new Promise(function(resolve, reject){ setTimeout(function(){ reject() }, 5000) }); |
Dikkat ettiyseniz, birinci örnekte ikinci bir parametre olmadığı için reject’i ihmal edebilirsiniz. Eğer reject’i kullanmak istiyorsanız, resolve’yi ihmal edemezsiniz. Aşağıdaki kod çalışmayacaktır. ve Promise resolved olarak bitecektir.
1 2 3 4 5 6 7 8 9 10 11 |
// Can't omit resolve ! const myPromise = new Promise(function(reject){ setTimeout(function(){ reject() }, 5000) }); |
Şimdi, Promise çok kullanışlı görünmüyor değil mi? Bu örnekler kullanıcı için ekrana bir şeyler yazmıyor. İşleri birazda karıştırmak için biraz veri ekleyelim. resolved ve rejected Promise sonuç(data) dönebilir. Örneğimiz :
1 2 3 4 5 6 7 |
const myPromise = new Promise(function(resolve) { resolve([{ name: "Chris" }]); }); |
Ama yine de herhangi bir veri göremiyoruz. Promise’den veriyi ayıklamak için zincirleme metodu olan then’i çağırmamız gerekiyor. İşin komik yanı, gerçek veriyi almak için then
bir tane callback fonksiyonu alır.
1 2 3 4 5 6 7 8 9 10 11 |
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then(function(data) { console.log(data); }); |
Bir JavaScript geliştiricisi olarak yazdığımız kodlarda ve başka yazılımcıların kodunu kullandığınız zaman çoğunlukla Promise’lerle iletişime geçeceksiniz. Bir çok kütüphane geliştiricisi, kodlarını Promise constructor ‘ı ile sarmalayacaktır. Örneğin:
1 2 3 4 5 6 7 8 |
const shinyNewUtil = new Promise(function(resolve, reject) { // do stuff and resolve // or reject }); |
İhtiyaç durumunda, Promise.resolve()’yi kullanarak yeni bir Promise yaratıp resolve edebiliriz.
1 2 3 4 5 6 |
Promise.resolve({ msg: 'Resolve!'}) .then(msg => console.log(msg)); |
Tekrar özetlersek: JavaScript Promise gelecekte olacak olaylar için bir yer işaretidir. Olay pending statüsü ile başlar ve ya başarılı olabilir( resolved, fulfilled) yada başarısız olabilir (rejected). Promise veri dönebilir. Ve bu dönen veri then kullanılarak yakalanabilir. Bir sonraki bölümde Promise’de hatalarla nasıl başa çıkıldığını göreceğiz.
ES6 Promise’lerde hata işlemleri
Hata yakalama işlemleri en azından eş zamanlı (synchronous) JavaScript kodunda her zaman kolay olmuştur. Aşağıdaki kodu ele alalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function makeAnError() { throw Error("Sorry mate!"); } try { makeAnError(); } catch (error) { console.log("Catching the error! " + error); } |
Çıktı şu şekilde olacaktır:
1 2 3 4 5 |
Catching the error! Error: Sorry mate! |
Hata iç blokta alındı. Şimdi aynı işlemi eş zamansız (asynchronous) fonksiyonda deneyelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function makeAnError() { throw Error("Sorry mate!"); } try { setTimeout(makeAnError, 5000); } catch (error) { console.log("Catching the error! " + error); } |
Yukarıdaki kod setTimeout
kullanıldığı için eş zamansızdır (asynchronous). Kodu çalıştırırsak ne olur.
1 2 3 4 5 6 7 8 9 |
throw Error("Sorry mate!"); ^ Error: Sorry mate! at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9) |
Yukarıdaki hatalı durumla başa çıkabilmek için callback alan catch’i kullanabiliriz.
1 2 3 4 5 |
const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); myPromise.catch(err => console.log(err)); |
Bunun yerine Promise yaratıp rejecting yapmak için Promise.reject() kullanabiliriz.
1 2 3 4 5 |
Promise.reject({msg: 'Rejected!'}).catch(err => console.log(err)); |
Tekrar özetlersek: then metodu; Promise başarılı (fullfilled) ise çalışır. Hata olduğundan catch metodu(handler) Promise’yi rejected yapmak için çalışır. Hikaye henüz bitmedi. Bir sonraki bölümde async/await’in try/catch ile nasıl çalıştığını göreceğiz.
ES6 Promises combinators: Promise.all, Promise.allSettled, Promise.any ve arkadaşları
Promise’ler tek başına değillerdir. Promise API’si Promiseleri birbirine bağlamak için bir çok metot içerir. Bunlardan en çok kullanışlı olanı; içinde Promise’lerin bulunduğu listeyi alıp tek bir Promise olarak dönen Promise.all
metodudur. Eğer Promise’lerden biri rejected olursa Promise.all’da rejected olur.
Promise.race
ise Promise listesindeki Promise’lerden biri yerleşir yerleşmez; resolve
yada reject
yapar.
V8’in yeni versiyonunda Promise.allSettled ve Promise.any metodları gelecek.
Promise.allSettled ise en ilginç olanı. Promise listesini parametre olarak alır. Eğer içlerinde biri rejected olsa bile kısa devre yapmaz. Promise listesindeki en az birinin yerleşik olup olmadığını kontrol etmek için faydalıdır. Promise.all’ın benzeri gibi düşünebilirsiniz.
ES6 Promises ve Microtask Queue
Bir önceki bölümlerden hatırlarsanız; her JavaScript moturunda eş zamansız (asynchronous) callback fonksiyonu, Call Stack’e gönderilmeden önce Callback Queue’de son bulur. Ama Promise’lerde kullanılan callback fonskiyonunun başka bir kaderi vardır. Bu callbackler, Callback Queue’de Microtask Queue tarafından ele alınır.
Dikkat etmeniz gereken çok garip bir durum var. Microtask Queue; Callback Queue’ye göre önceliklidir. Microtask Queue’den gelen callbackler Event Loop’un kontrollünden sonra Call Stack’e iletilecek callbacklerden önceliklidir.
Altında yatan mekanizma; Jake Archibald tarafından Tasks, microtasks, queues and schedules makalesinde detaylı bir şekilde anlatılmıştır. Okumanızı tavsiye ederim.
JavaScript Motorları: Nasıl Çalışıyorlar? Asynchronous evrim: Promises’de async/await’e
JavaScript her sene, dile yeni iyileştirmeler konusunda hızlı hareket ediyor. Promise’ler bir varış noktası gibi görünüyordu ama ECMAScript 2017 (ES8) ile birlikte async/await doğdu.
async/await; söz dizimsel olarak şeker dediğimiz bir geliştirme. async/await JavaScript’te hiç bir değiklik yapmıyor(Unutmayın, JavaScript eski tarayıcılarla uyumlu olmalı ve çalışan kodu bozmamalı)
Promises’lerin yazılımı ile ilgili yeni bir eş zamansız (asynchronous) kod yazma yöntemidir. Bir örnek yapalım. Öncelikle Promise ile kod yazalım.
1 2 3 4 5 6 7 8 9 |
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then((data) => console.log(data)) |
Şimdide async/await ile de, kodu okuyan birine göre eş zamanlı (synchronous) kodmuş gibi görünen bu eş zamansız( asynchronous) kod yazabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); async function getData() { const data = await myPromise; console.log(data); } getData(); |
Mantıklı mı? Asıl komik olan ise async her zaman Promise döner. Kimse buna engel olamaz.
1 2 3 4 5 6 |
async function getData() { const data = await myPromise; return data; } getData().then(data => console.log(data)); |
Peki ya hatalar konusu nasıl oluyor? Async/await sunduğu nimetlerden birisi’de try / catch kullanımıdır. (an introduction to handling errors in async functions and how to test them). Promise’lerde try/catch kullanımı ile bir örnek yapmıştık:
1 2 3 4 5 6 7 8 9 |
const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); myPromise.catch(err => console.log(err)); |
Aynı kodu async kullanarak tekrar yazalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
async function getData() { try { const data = await myPromise; console.log(data); // or return the data with return data } catch (error) { console.log(error); } } getData(); |
Herkes bu koda tav olmayabilir. try/catch kodda biraz göze batıyor. try/catch kullanırken bu noktada bir gariplik daha var. Kodun iç kısmında hata fırlatılan aşağıdaki kodu düşünelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
async function getData() { try { if (true) { throw Error("Catch me if you can"); } } catch (err) { console.log(err.message); } } getData() .then(() => console.log("I will run no matter what!")) .catch(() => console.log("Catching err")); |
Konsola ne yazılacak? Unutmayın ki try/catch eş zamanlı (synchronous) bir yapıdadır ama eş zamansız ( asynchronous) fonksiyonumuz Promise’dir. İki farklı yolda, giden iki farklı trenler gibi.
Ama hiç bir zaman karşılaşmayacaklar. Yani, getData() tarafından fırlatılan hata hiç bir zaman tetiklenmez . Yukarıdaki kodun sonucu “Catch me if you can” ardından “I will run no matter what!” şeklinde olacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
async function getData() { try { if (true) { return Promise.reject("Catch me if you can"); } } catch (err) { console.log(err.message); } } |
Şimdi hata beklediğimiz gibi çalışacaktır.
1 2 3 4 5 6 7 8 9 |
getData() .then(() => console.log("I will NOT run no matter what!")) .catch(() => console.log("Catching err")); "Catching err" // output |
async/await, Javascript’te eş zamansız (asynchronous) kod inşa etmek için en iyi yol gibi görünüyor. Hata yakalama işlemlerinde daha kontrol sahibiyiz ve kodumuz daha temiz görünüyor.
JavaScript Motorları: Nasıl Çalışıyorlar? Toparlama
JavaScript motorları Call Stack, Global Memory, Event Loop, Callback Queue gibi hareketli bir çok parçalara sahiptir. Bütün parçalar eş zamanlı (synchronous) ve eş zamansız (asynchronous) kod yazımı için birlikte çok iyi çalışmaktadır.
JavaScript motorları ingle-threaded’tır. Bunun anlamı; fonksiyonları çalıştırmak için tek Call Stack bulunur. Bu kısıtlama JavaScript’tin eş zamansız( asynchronous) doğasının bir sonucudur. Zaman gerektiren tüm işlemler, harici bir birim tarafından yürütülmelidir( örneğin tarayıcılar) veya callback fonksiyonu tarafından.
Daha basit kod akışı için ECMAScript 2015 ile birlite Promise’ler geldi.Promise eş zamansız( asynchronous) bir nesnedir ve eş zamansız işlemlerin başarılı veya hatalı durumlarını tarif etmek için kullanılır. Fakat geliştirmeler burada durmadı. 2017’de async/await doğdu: Promiseler için, eş zamansız kodları, eş zamanlıymış gibi yazmayı sağlayan bir artistik bir makyaj.
Okuduğunuz için teşekkür ederim ve sayfamı takip etmeye devam edin.
Çeviri notları:
Makalede gözünüze çarpan çeviri hatalarını iletirseniz gerekli düzenlemeleri yaparım. Başka bir çeviride görüşmek üzere.