ES6 generator ve ES7 async fonksiyonlar

Bu repository altında ES6’da bulunan generatör fonksiyonlarını ve ES7’de
bulunan async fonksiyonlarını inceleyeceğiz. Yazının bir kopyasına Github adresim üzerinden ulaşabilirsiniz.

ES6 – Generatörler

Nedir bu generatorler?

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ı.

Yield

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.

for-of altında generatörler

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, throw ve return

.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}

Yield ve asenkron işler

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}

ES7 – Async fonksiyonlar

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 keywordune null verirsek bir hataya neden olmayacaktır.

Not: await keywordune promise harici bir değer verirsek direkt
olarak pass edecektir yani let t = await 1; yaparsanız t değeri direkt
1 olacaktır. Normal fonksiyonları versenizde aynı şekide pass işlemi
yapılacaktır.

No Comments

Post a Comment

Comment
Name
Email
Website