Bu repository altında ES6'da bulunan generatör fonksiyonlarını ve ES7'de bulunan async fonksiyonlarını inceleyeceğiz.
Generatörler adlandırıldığı gibi bir takım fonksiyondur. Normal bir fonksiyondan farklı olarak çalışma zamanından kaldığı yerden devam etme özelliği taşırlar. Bu sayede bir işi üretirken bir anda döndürmek yerine parça parça döndürebilme yeteneğine sahiptirler.
Generatör fonksiyonumuzu tanımlarken normal fonksiyona ek, *
(yıldız) karakterini kullanmamız gerekiyor.
function* generator() {
}
Bu generator fonksiyonumuzu çağırdığımız taktirde generator'ün gövdesinde tanımlı olan kod parçası çalışmaz.
function* generator() {
console.log("merhaba");
}
generator();
Bunun yerine generator fonksiyonu özel başka bir obje döndürüyor. Eğer dönen bu objenin .next()
metodunu çağırırsanız merhaba yazısını görebileceksiniz.
function* generator() {
console.log("merhaba");
}
var g = generator();
g.next(); // "merhaba" yazıldı ve {value: undefined, done: true} döndürüldü
Gördüğünüz gibi metod başka bir obje döndürdü. Burada value
undefined olarak atanmış ve done
ise true. done
ifadesi generatör fonksiyonun tüm işlevinin bittiğini gösteriyor. value
ise return edilen değeri getiriyor.
Buraya kadar normal fonksiyonun dışında farklı bir işlem göremedik ancak işler daha yeni kızışmaya başladı.
Generatörlerin done
değeri döndürdüğünü gördük, demek ki biz generatörleri bir şey kullanarak durdurabiliyoruz. yield
bu işi yapmamızı sağlayan özelliktir.
yield
tıpkı return gibi fonksiyondan çıkışı sağlar ancak returnun aksine çıkılan bu fonksiyona tekrar girişi mümkün kılar. Sadece bu da değil; çıkılan noktaya dışardan değer girilebilmesinide sağlar.
function* generator() {
console.log("merhaba");
yield;
}
var g = generator();
g.next(); // "merhaba" yazıldı ve {value: undefined, done: false} döndürüldü
Gördüğünüz gibi bu sefer done
değeri false oldu. Eğer bir defa daha .next()
metodunu çalıştırırsak;
function* generator() {
console.log("merhaba");
yield;
}
var g = generator();
g.next(); // "merhaba" yazıldı ve {value: undefined, done: false} döndürüldü
g.next(); // {value: undefined, done: true} döndürüldü
done
değeri true
olacak. .next()
metodu çağrıldığında yield
ifadesine gelinceye kadar tüm kodlar sırayla çalıştırılır. yield
ifadesi geldiğinde eğer return gibi sağında bir değer işlem varsa yapılır, döndürdüğü değer ise .next()
methodunun döndürdüğü objenin value
property'sine yazılır.
Eğer bir değer döndürmek istersek yield
'in sağına yazabiliriz.
function* generator() {
console.log("merhaba");
yield 8;
yield 9;
return 10;
}
var g = generator();
g.next(); // {value: 8, done: false}
g.next(); // {value: 9, done: false}
g.next(); // {value: 10, done: true}
Bu fikirden yola çıkarak fibonacci sayılarını
generate edebiliriz.
function* fibonacci() {
let a = 1, b = 1;
for(;;) {
yield a + b;
let t = b;
b = a + b;
a = t;
}
}
var g = fibonacci();
g.next(); // {value: 2, done: false}
g.next(); // {value: 3, done: false}
g.next(); // {value: 5, done: false}
g.next(); // {value: 8, done: false}
Bu generatorümüzün herhangi bir sonu yok. Yani done
asla true olmayacak.
Generatörlerimizi for-of
syntaxı içinde kullanmamız mümkündür. Ancak bir önceki örnekte olduğu gibi sonsuz olmasından kaçının. Her bir iteration'da .next()
çağrısı yapılacak ve döndürülen değer değişkene yansıtılacak. For gövdesindeki işler tamamlandıktan sonra tekrar .next()
çağrısı yapılacak. done
true
olduğu an döngü kırılacak.
function* fibonacci(limit) {
let a = 1, b = 1;
while(limit--) {
console.log("bir sonraki :" + (a + b));
yield a + b;
let t = b;
b = a + b;
a = t;
}
console.log("bitti");
}
for(sayi of fibonacci(10)) {
console.log(sayi);
}
Bu kodumuzun konsol çıktısı;
bir sonraki :2
2
bir sonraki :3
3
bir sonraki :5
5
bir sonraki :8
8
bir sonraki :13
13
bir sonraki :21
21
bir sonraki :34
34
bir sonraki :55
55
bir sonraki :89
89
bir sonraki :144
144
bitti
Bir örnek daha verelim. for-of syntaxını dizilerin değerlerini okurken kullanıyoruz.
let dizi = [1, 2, 3];
for (let eleman of dizi) {
console.log(eleman); // 1 .. 2 .. 3
}
Ancak objelerin key ve value'leri arasında gezemiyoruz.
let obje = {a: 1, b: 2};
for (let eleman of obje) { // Uncaught TypeError: undefined is not a function
console.log(eleman);
}
Bu problemi generator fonksiyonuyla çözebiliriz,
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
let obje = {a: 1, b: 2};
for (let [key, value] of entries(obje)) {
console.log(key, value); // a, 1 .. b, 2
}
.next()
metodunun bir parametre girişi vardır. Bu girişten vereceğimiz sayı ile yield
alanına sayı göndermemiz mümkündür.
function* say() {
var baslangic = yield;
console.log(baslangic);
}
var s = say();
s.next(); // yield alanına kadar next yapıyoruz.
s.next(3); // 3
.throw()
metodu tıpkı next gibi kodun bulunduğu yerde throw
yapar.
function* say() {
try {
yield Math.random();
} catch(e) {
console.error(e); // 0.5'ten büyük olmamalı
}
}
var s = say();
if(s.next().value > 0.5) {
s.throw(new Error("0.5'ten büyük olmamalı"));
}
.return()
metodu generatorü sonlandırarak ilk parametredeki değeri value olarak döndürür.
function* say() {
console.log(1);
yield 1;
console.log(2);
yield 2;
console.log(3);
yield 3;
}
var s = say();
s.next(); // {value: 1, done: false}
s.return(4); // {value: 4, done: true}
s.next(); // {value: undefined, done: true}
Bence tüm bu olayların dışında yield
'ın en güzel olayı asenkron işleri yapmakta çatı sağlaması. Şimdi teorik olarak düşünelim. yield
bir fonksiyon generatörünü durduruyor. Sadece durdurmakla kalmıyor bize veride veriyor. Biz yield
'da promise
versek, ve bu promise
bittiğinde başka bir next çağırmasını sağlasak callbacklerden kurtulmuş olmaz mıyız?
function* asenkron() {
yield new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, 1000);
});
console.log("merhaba");
}
var s = asenkron();
var promise = s.next().value; // {value: Promise, done: false}
promise.then(function() {
s.next();
});
Bu kod ile 1 saniye bekledikten sonra merhaba yazdırdık. Öyle bir fonksiyon tasarlayalım ki bu işi bizim yerimize o yapsın.
function run(g) {
var i = g.next(); // bir sonraki değeri getir
if (!i.done) { // eğer bitmediyse
if (i.value && i.value.constructor == Promise) { // dönen değer promise ise
i.value.then(function (data) { // promise'in bitmesini bekle
run(g); // yeniden kendini çağır
}).catch(function (e) { // hata oluştuysa
g.throw(e); // ilgili yerde hata fırlat
});
} else { // eğer promise değilse sadece devam et
run(g);
}
}
}
Tabi ki bu kodu inanılmaz derecede basit tuttum ve bu yüzden de bellek problemleri var. Kullanmak için tek yapılması gereken run(generator())
çağrısı yapmak. Daha önce tanımladığımız delay işlemini fonksiyon halinede getirelim.
function delay(time) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, time);
});
}
function* merhaba() {
yield delay(1000);
console.log("merhaba");
yield delay(1000);
console.log("dünya");
}
run(merhaba());
1 saniye aralıklarla merhaba dünya yazdırdık.
Bu run methodu bazı işleri yapmak için yetersiz. Bu durumlarda co kütüphanesi gibi kütüphaneler kullanabilirsiniz. Bu kütüphanelerin nasıl yazıldığıyla daha fazla bilginiz olsun diye yukardakinden daha gelişmiş bir run methodu yazdım.
Fonksiyonu anlamak biraz güç olabilir ancak bu ve benzeri kütüphaneler aşağıdaki işleri yapabilirler.
- Promise döndürerek bitişi takip etmek.
run(generator()).then(function() {
});
- Promise hata verdiğinde ilgili satırda exception fırlatımı
- Kod içerisinde başka bir generator fonksiyonu çağırabilirsiniz. örneğin;
function* taskA() {
console.log("taskA");
yield taskB();
console.log("taskA bitti");
}
function* taskB() {
console.log("taskB");
yield delay(1000);
console.log("taskB bitti");
}
run(taskA()).then(function() {
console.log("tüm tasklar bitti");
});
taskA
taskB
* 1 saniye bekler
taskB bitti
taskA bitti
tüm tasklar bitti
- Dizi verilerek birden fazla taskın bitmesini bekleyebilirsiniz
function* taskA() {
yield delay(2000);
console.log("taskA");
yield delay(1000);
console.log("taskA bitti");
return "A";
}
function* taskB() {
yield delay(500);
console.log("taskB");
yield delay(500);
console.log("taskB bitti");
return "B";
}
function* taskX() {
console.log("taskX");
yield delay(1000);
let arr = yield [taskA(), taskB()];
console.log(arr);
yield delay(1000);
console.log("taskX bitti");
}
run(taskX());
Ekleme:
yield
keywordu eğer bir*
yıldızla kullanılırsa sağında verilen nesnenin yield özelliklerini öğrenebilir. Örneğin;function* say() { yield 1; yield 2; } function* okubakim() { yield* say(); } let oku = okubakim(); oku.next(); // {value: 1, done: false} oku.next(); // {value: 2, done: false} oku.next(); // {value: undefined, done: true}Ayrıca
yield*
dizi değerlerini tek tek verebilir ve string'in karakterlerini tek tek okuyabilir.function* ayi() { yield* "ayi"; } let neymis = ayi(); neymis.next(); {value: 'a', done: false} neymis.next(); {value: 'y', done: false} neymis.next(); {value: 'i', done: false} neymis.next(); {value: undefined, done: true}
ES6'da asenkron işleri yönetmenin kolay yolunu gördük. Ancak yield'i direkt olarak fonksiyon çağrısı yaparak kullanamıyoruz. run()
methoduna ihtiyacımız var. ES7'de bu ihtiyaç kaldırılarak async function
syntax'ı gelmiştir.
async function merhaba() {
}
Yukardaki gibi asenkron fonksiyon tanımlaması yapabiliriz.
async function merhaba() {
console.log("merhaba");
}
merhaba();
ES6'da run()
methodunu kullanmamız gerekirken, burada direkt olarak fonksiyon çağrısı yaptık. Asenkron fonksiyonları sanki promise yapısına çeviren bir keyword olarak düşünebiliriz.
async function selam() {
console.log("merhaba");
}
// aşağıdaki gibi derleniyor
function selam() {
return new Promise(function(resolve, reject) {
try {
console.log("merhaba");
resolve();
} catch(e) {
reject(e);
}
});
}
// asenkron fonksiyon mutlaka bir promise döndürür.
selam().then(function() {
console.log("çalıştı");
})
Peki bu async fonksiyonlarla neler yapabilirim. ES6'da yield
'ı asenkron işleri bekletmek için kullanabiliyorduk. ES7'de bu iş için await
keywordu bulunmaktadır.
function delay(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms)
});
}
async function selam() {
console.log("merhaba");
await delay(1000);
console.log("dünya");
}
selam();
async fonksiyonlar promise döndürdüğü için bir async fonksiyonu içinde await ile başka bir async fonksiyonu bekletebiliriz.
async function taskA() {
console.log("A");
let b = await taskB();
return "A" + b;
}
async function taskB() {
console.log("B");
return "B";
}
taskA().then(function(sonuc) {
console.log(sonuc); // AB
})
Örnektede görüldüğü gibi await'i herhangi bir expression içinde kullanmamız mümkün. Bu sayede bir değişkene değer atayabiliriz. Döndürdüğümüz değer await'in bulunduğu konuma yerleştirilecektir.
Önemli Not: Bu tüm işlemler sırasında try catch kullanmanız şiddetle önerilir. İlerki Node.js sürümlerinde eğer promisede oluşmuş bir hata varsa ve catch ile yakalanmamışsa uygulama çökmüş gibi
process.exit()
işlemi yapılacaktır.
Not:
await
keywordunenull
verirsek bir hataya neden olmayacaktır.
Not:
await
keywordune promise harici bir değer verirsek direkt olarak pass edecektir yanilet t = await 1;
yaparsanız t değeri direkt 1 olacaktır. Normal fonksiyonları versenizde aynı şekide pass işlemi yapılacaktır.