Oca.20

Hızlandırılmış EcmaScript 6+ Dersleri – 12: Promise (Söz)


Promise, yani Türkçe karşılığı ile “Söz Vermek”, JavaScript’te bir işi yapmak için istekte bulunduğumuzda bu işin gerçekten yapılıp yapılmadığına dair karşı tarafın bize söz vermesi anlamına gelir. Bir iş yapmaya zorlanır, ancak yapılamazsa da yine bu durum haber verilir. Böylece istek yapıldığında oldu mu olmadı mı kontrolü yapmak ve buna göre programın akışına devam etmek oldukça önemlidir.

Promise olmadan önce bir şekilde Event yönetimi yaparak bir işlemin başarılı olup olmadığını kontrol edebiliyorduk, ancak oldukça elverişsiz ve çöp kod oluşmasına neden oluyordu.

Mesela bir resmi sayfaya yüklemek istediğimizde yüklenip yüklenmediğini kontrol edeli…

var logo = document.querySelector('.logo-resim');
logo.addEventListener('load', function() {
  // resim  yüklendi.
});

logo.addEventListener('error', function() {
  // resim yüklenemedi.
});

.logo-resim Class isimli tag’e src attribute’ünde verilen kaynağın yüklenip yüklenmediğini load ve error Event’leri ile kontrol eden bir yapı yer almaktadır.

Promise Kavramı

P romise nesnesi new Promise() şeklinde oluşturulur ve bir fonksiyonu parametre olarak alır. Fonksiyon da temelde iki adet parametre alır ve bunlar Promise’in sonuçlanan durumlarıdır. Promise’in istek yapmasına da pending (askıda) denir. Eğer istek başarılı olursa resolved (kararlı) başarısız olursa da reject (red edilen) durumları oluşur.

Promise, asenkron çalışmaktadır. JavaScript’te eskiden kullanılan callback’in yerine geçmiştir (Eski olduğu için kitapta değinmedik).

Promise, aşağıdaki durumlarda kullanılır, hangi durumlarda gerek yokturdur.

  • Bir isteğin sonucunu beklediğimizde. Genelde API isteklerinde veya cevabı uzun dönen fonksiyonları beklerken kullanılır. Sonucu hemen dönen işlemlerde kullanma ihtiyacı yoktur
  • Çok fazla if, else, else if gibi koşullar kullanmadan daha temiz bir kod yazmak gerektiğinde (gerekiyor da).

Kısacası; isteğe cevabın uzun döneceği veya hiç dönmeyeceği yapılarda kodun daha kısa ve temiz olması gereken hallerde kullanmalıyız.

Promise Yapısı ve Kullanım Örnekleri

Aşağıda, en basit yapısı ile bir Promise görülmektedir.

let promise = new Promise( (resolve, reject) => {
    // Bir şeyler yap...
    // if ( /* Her şey yolundaysa */ ) {
    if (1==1) resolve("Her şey yolunda.");
    else reject("Bir sorun var!");
});

promise.then( mesaj => {
    console.log(mesaj); // "Her şey yolunda."
}, () => {
    console.log(mesaj); //Bir sorun var!"
});

Yazdığımız Promise yapısını inceleyelim…

İlk olarak promise adında bir Promise nesnesi oluşturduk. Parametre olarak bir arrow fonksiyon oluşturup (normal fonksiyon da kullanabilirsiniz, kafanız karışmasın), parametre olarak resolved ve reject fonksiyonlarını parametre olarak verdik. Fonksiyon içinde bir şeyler yaptığımızı düşünelim. Mesela bir yere bir istek attık ve sonucu bekliyoruz. Veya bir Web Worker’dan dönüş bekliyoruz. Veya setInterval ile gecikmeli bir iş yaptık… İşlemin sonucuna göre bir true/false değeri oluşturduğumuzu düşünelim. Bu değere göre if veya else koşuluna giriyoruz. Örnek olması amacı ile 1 == 1 kontrolünü yapıp true değeri ile if bloğuna girdik. Durum başarılı olduğu için aldığımız resolve fonksiyonu çağırıp parametre olarak “Her şey yolunda.” String’ini verdik. Eğer durum false olsaydı else bloğunda reject fonksiyonu çağırıp parametre değerine “Bir sorun var!” String’ini verecektik.

Promise, resolved olduğunda doğrudan .then metodunu tetikler.

Buraya kadar Promise yapımız tamam. Bu Promise çalıştığında bunu da kontrol eetmemiz gerekiyor. Bunu da .then metodu ile yapıyoruz. Promise çalıştığında ve resolved veya rejected durumları tamamlandığında then’e gelir. Burada da mesaj parametresi ile Promise’ten dönen String’i alıyoruz. Birinci arrow fonksiyonu resolved ile çalışarak olumlu durumu, ikinci arrow fonksiyonu da olumsuz durumu işler.

Promise nesnesine resolved ve rejected isimli iki fonksiyonu parametre veriyoruz. Ancak burada aslında önemli olan bu fonksiyonları isimleri değil, fonksiyonları sırasıdır. Yani iki paremetreden birincisi olumlu sonucu, ikincisi de olumsuz sonucu karşılar. resolved yerine olumlu, rejected yerine olumsuz isimlerini de yazabilirsiniz. Ancak genel itibari ile İngilizce terimler kullanıp genel görmüş yapı ve isimleri kullanırsanız kodunuz daha anlaşılır ve evrensel olur.

Şimdi gerçek bir örnek yapalım…

Bu sefer kod içindeki açıklama satırlarından takip edin.

// veriGetir isminde bir arrow fonksiyonu tanımlıyoruz.
// Bu fonksiyon adres isminde bir parametre alıyor.
let veriGetir = adres => {
    // Fonksiyon bir Promise nesnesi geri döndürüyor.
    // Promise nesnesi resolved için başarılı,
    // rejected için ise basarisiz fonksiyonlarını alıyor.
    return new Promise( (basarili, basarisiz) => {
        // istek isminde bir XMLHttpRequest nesnesi oluşturuyoruz.
        let istek = new XMLHttpRequest();
        // Bu nesne ile GET metodunu kullanarak adres parametresi ile verilen URL'e çağrı yapıyoruz.
        istek.open("GET", adres, true);

        // İstek yüklendiğinde...
        istek.onload = () => {
            // Eğer Status kodu 200 ise, yani yükleme başarılıysa...
            // true sonucu üretilecek ve if bloğuna girecek.
            if (istek.status == 200) {
                // baarili isimli fonksiyona gelen sonuç gönderiliyor.
                basarili(istek.response);
            } else {
                // Eğer 200'den farklı bir sonuç gelirse basarisiz fonksiyona gidiliyor.
                basarisiz("Veriler yüklenemedi");
            }
        }

        // Eğer yüklemede bilinmeyen bir hata olursa yine basarisiz fonksiyonuna gidiliyor.
        istek.onerror = () => {
            basarisiz("Sorun var!");
        }
        // İstek gerçekleştiriliyor...
        istek.send();
    });
}

// veriGetir fonksiyonunun bir Promise nesnesi döndürdüğüne dikkat edin.
// Yani veriGetir bir fonksiyon'dan bir Promise nesnesi gelir.
// Parametre olarak JSON veri dönen bir adrese istek yapılıyor.
//.then metodu ile gelen JSON verisi veri isimli parametre ile yazdırılıyor.
// Eğer rejected olursa da hata durumu mesaj parametresi ile yazdırılıyor.
veriGetir("https://jsonplaceholder.typicode.com/todos/")
    .then( veri => {
        console.log(veri);
    }).catch(mesaj => {
        console.log(mesaj);
    }
);

Konsolda JSON verileri yazdırılacaktır.

Zincirleme .then Kullanımı

Bir Promise nesnesinden dönen resolved sonucu ile .then metodunu çağırmayı öğrenmiştik. Ancak .then metodunu zincirleme olarak, yani peş peşe de kullanabiliriz.

Örneğin bir Promise nesnesini düşünün. Kendisi çağırıldığında 1000 milisaniye sonra işlem yapsın ve sonucu işlesin.

new Promise( (resolve, reject) => { 

    setTimeout(() => { 
        resolve(1); 
    }, 1000);

})
.then( sayi => { 
    console.log('Sıra: ', sayi); 
    return ++sayi; 
})
.then(sayi => { 
    console.log('Sıra: ', sayi); 
    return ++sayi; 
})
.then(sayi => { 
    console.log('Sıra: ', sayi);
});

Örneğimizde 3 defa .then kullanımı görülüyor. Promise 1000 milisaniye sonra çalışacak ve sırası ile sayi değerini 1’er artırıp devam edecek. Sonuç konsolda aşağıdaki gibi yazdırılacaktır.

Sıra:  1
Sıra:  2
Sıra:  3

Birden Fazla Asenkron Çağrı Yapmak

Bir tane Promise nesnesi ile çalıştığımız gibi birden fazla Promise nesnesi ile de çalışmamız gerekebilir. Mesela birinci Promise nesnemiz bir sonucu üretirken, diğer Promise nesnemiz de başka bir sonuç üretsin. Aynı şekilde daha fazla Promise nesnemiz de farklı farklı sonuçlar üretsin. Nihayetinde bütün Promise nesnelerinin başarılı çalışıp çalışmadığını kontrol etmemiz gerekir. Eskiden bu işi yapmak için iç içe if döngüleri ile kontroller yapmamız gerekiyordu. Bu da kodumuzu aşağıdaki örnek şekilde gibi anlamsız bir şekilde iç içe yazılarak uzamasına neden oluyordu.

Peki, bu saçma durum için ne yapılabilir?

Promise nesnesinin .all metodu parametre olarak Promise nesnelerini dizi olarak alır. Her bir Promise nesnesi resolved sonucuna ulaştığında .all metodu çalışır.

Aşağıda Promise.all metodunun yapısı görülmektedir.

Promise.all([promise_1, promise_2])
.then(result => { 
    // Her iki Promise de resolved oldu. 
}) 
.catch(error => { 
    // Bir veya birden fazla Promise rejected oldu. 
});

Örnek olarak 3 adet Promise’i farklı farklı kullanarak hepsinin sonucunu işleyelim…

const p1 = Promise.resolve(1);
const p2 = true;
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(`3. promise başarılı.`);
    }, 1000 );
});

Promise.all([p1, p2, p3])
.then( degerler => { 
    console.log(degerler);
});

Birinci Promise doğrudan resolve metodunu çağırıp 1 değerini döndürmüş.

İkinci Promise de true olarak değerlendirilmiş.

Üçüncü Promise de 1000 milisaniye sonra çalışıyor (burası çok önemli!). Yani ilk ikisi hemen sonuç döndürürken, bu 1 saniye sonra resolve olarak cevap vermiş.

Promise.all metoduna dizi olarak [] içerisinde üç Promise de verilmiş. Her üçü de resolved döndüğü için .then metoduna girilmiş ve Promise sonuçları değerler parametresine aktarılmış. Sonra da konsolda yazdırılmış. Konsolda yazdırılan sonuç aşağıdaki gibidir.

Şimdi daha gerçekçi bir örnek yapalım. İki farklı API’den istek yapalım ve her ikisi de JSON döndüğünde bu verileri birleştirerek Promise.all metodunda yazdıralım.

const albumler = new Promise((resolve, reject) => {
    
    fetch('https://jsonplaceholder.typicode.com/albums', {
        method: 'get'
    }).then(function(response) {
        resolve(response.json());
    }).catch(function(e) {
        reject(e);
    });

});

const fotograflar = new Promise((resolve, reject) => {
   
    fetch('https://jsonplaceholder.typicode.com/photos', {
        method: 'get'
    }).then(function(response) {
        resolve(response.json());
    }).catch(function(e) {
        reject(e);
    });

});

Promise.all( [albumler, fotograflar] ).then(data => { 
    let muzikListesi = data;
    console.log( muzikListesi );
});

Örneğimizde albumler ve fotograflar adında 2 adet Promise bulunmaktadır. Her ikisi de resolved ve rejected durumlarını yönetmektedir. Her iki Promise de bir API adresinden JSON verisi çekmektedir. Burada farklı olarak fetch() ile JSON verilerini çektik. fetch, XMLHttpRequest gibi çalışır. Parametre olarak URL’i alır. İkinci parametre olarak da isteğin metodu yazılır. GET isteği yapacağımız için method:’get’ tanımı yaptık. fetch, kendi içinde de then ve catch metotlarına sahiptir. API dönüşü sağlanırsa .then’e girecek ve resolved fonksiyona gelen veriyi .json() metodu ile JSON verisine dönüştürüp gönderecek. Eğer API’den dönüş olmazsa .catch’e girecek ve hata kodunu e (yani error) parametresi ile gönderecek.

Sonuç olarak 100 adet şarkı ve 5000 adet fotoğraf verisi JSON olarak muzikListesi isimli JSON verisine birleştirilip aktarılarak konsolda yazdırıldı.

Makale serimizin diğer konuları şunlar olacaktır:


Web Tasarımı ve Web Programlama 2020

Yorum bırak

Yorum