Javascript ile Asenkron İşlemler

Merhabalar bugün ki yazım kısaca Javascript ile asenkron işlemlerin nasıl yönetileceği ve bunun zamanla nasıl geliştiği hakkında olacak.

  • Konular
    • Asenkron'da neyin nesi?
    • Peki nasıl yapıyoruz?
    • Asenkron Yönetim Teknikleri
      • Callback Hell
      • Callback Hell 2
      • Promise API
      • ES6 Generators
      • ES7 Async/Await

Örnekleri ES6 featurelarını henüz kullanmayan arkadaşlar için Arrow Functions gibi yeni gelen özellikleri kullanmadan yazacağım.

Hemen yazıyı dallandırıp budaklandırmadan ilk konumuzdan giriyorum.

Asenkron Nedir?

Bu işi şu örnekle anlatabiliriz; Örneğin akşam eve geldiniz ve karnınız aç kendinize makarna yapacaksınız ardından da güzel demli bir çay içeceksiniz çayı da demlemek lazım tabi. Şöyle yapabiliriz,

makeSuperAwesomeFettuccinePasta(); // Makarnamız hazır ama çay için bekleyeceğiz daha :(  
makeStronglyBrewedTea(); // Çayımız hazır, aaa yemeği yemeyi unuttuk :(  
eat(); eat(); eat(); eat(); eat(); // Yummmmy :)  

Şimdi burada dikkat etmemiz gereken nokta biz makarnayı yapmaya başladık ama bizim makarnayı yapma işlemimiz bizim çayı yapma işlemimizi bloklanmış (blocking) hale getirdi.

Makarnamız bitip hazır olmadan çay yapma işlemine geçemiyoruz, gerçek hayatta ne yapardık işlemler arasında Context Switch yapardık.

Javascript dünyasında şuan için gerçek anlamda bir Multi Threading implementasyonu bulunmuyor, ne kadar gelen Web Worker özellikleriyle bazı limitasyonlarla beraber desteklese de gerçek anlamda desteklenmiyor.

Single Threaded çalışan Javascript bizim asenkron işlemlerimizi yapmamız için kendi içinde bunu Event Loop denilen bir mekanizmayla yönetiyor:

Philip Roberts (JS Conf 2014) sunumundan bu konuyu yarım saat içinde öğrenebilirsiniz:
https://www.youtube.com/watch?v=8aGhZQkoFbQ

Peki Nasıl Yapıyoruz?

Asenkron işlemleri anlamak için en basit anlamıyla setTimeout veya setInterval fonksiyonlarıyla başlayabiliriz.

console.log('Im just creepy kiddo');

setTimeout(function () {  
    console.log('Hello im async kiddo');
}, 500);

console.log('Im just normal kiddo');  

Bu kodun çıktısı aşağıdaki gibi olacaktır:

Im just creepy kiddo  
Im just normal kiddo  
Hello im async kiddo  

Gördüğünüz gibi bizim async kiddo 500 milisaniye geç geldi ve normal kodun yukarıdan aşağıya olan akışını bir anda yok etti :)

Aslında burada olan şey setTimeout fonksiyonunun çağrıldığı anda geriye bir Timer Id ile hemen dönüş yapması, daha sonra dilerseniz bu id ile clearTimeout fonksiyonunu kullanarak timeout'unuzu yok edebilirsiniz. Bu bahsettiklerimin hepsi setInterval içinde geçerlidir setInterval sadece clearInterval kullanılmadığı sürece sürekli çalışmaya devam eder belirtilen süre aralıklarıyla.

Burada dikkat edilmesi gereken nokta bizim verdiğimiz 500ms süresi aslında garanti edilebilen bir süre olmamasıdır.

Bunun için genelde bu tarz işlemler yapılırken Date sınıfı kullanılarak en son çağrılma zamanı ile o an çalışma süresi arasındaki fark bulunarak işlem yapılır, zaman kritik işlemler yaparken bu konuya dikkat etmek gerekmektedir.

Asenkron Yönetim Teknikleri

Genel olarak vereceğim tüm örneklerde bir senaryo üzerinde yoğunlaşacağım:

Bir detay sayfasındayız ve kullanıcının profil bilgilerini, daha sonra arkadaşlarını ve en son olarak kullanıcının kazandığı madalyaları getireceğiz. En sonunda tüm verileri aldıktan sonra aksiyon alacağız.

Varsayalım ki Restful Api yollarımız aşağıdaki gibi olsun:

Profil: https://api.batikansenemoglu.com/v1/profile/blueskan
Arkadaşlar: https://api.batikansenemoglu.com/v1/profile/blueskan/friends
Madalyalar: https://api.batikansenemoglu.com/v1/profile/blueskan/medals


Callback Hell
$.ajax({
    url: 'https://api.batikansenemoglu.com/v1/profile/blueskan',
    success: function (response) {
        console.log(response); // Do some fancy things with profile response

        $.ajax({
            url: 'https://api.batikansenemoglu.com/v1/profile/blueskan/friends',
            success: function (response) {
                console.log(response); // Do some fancy things with friends response

                $.ajax({
                    url: 'https://api.batikansenemoglu.com/v1/profile/blueskan/medals',
                    success: function (response) {
                        console.log(response); // Do so fancy things with medals, everyone loves
                    },
                    error: function (err) {
                        console.error(err);
                    }
                });
            },
            error: function (err) {
                console.error(err);
            }
        });
    },
    error: function (err) {
        console.error(err);
    }
});

Bu ilk asenkron yönetim tekniğimizde ve kendisi aslında bir Anti-Pattern yani mümkün olduğunca kullanılmaması gereken yöntemlerden biri :)

Şimdi $.ajax fonksiyonu jQuery paketinin içinde XMLHttpRequest API'sini daha kolay kullanmamızı ve uzaktaki sunucuya istek yapıp cevap almamızı sağlıyor (Cevap verirse tabi :)). Bizim burada yaptığımız bu asenkron işlemleri her bir callback içinde sırasıyla çalıştırıyoruz, fakat bunu okuması çok zor. O yüzden hemen ikinci yöntemimize geçelim.

Callback Hell 2
var errorHandler = function (err) {  
    console.error(err);
};

var getProfile = function (response) {  
    console.log(response); // Do some fancy things with profile response

    $.ajax({
        url: 'https://api.batikansenemoglu.com/v1/profile/blueskan/friends',
        success: getFriends,
        error: errorHandler
    });
}

var getFriends = function (response) {  
    console.log(response); // Do some fancy things with friends response

    $.ajax({
        url: 'https://api.batikansenemoglu.com/v1/profile/blueskan/medals',
        success: getMedals,
        error: errorHandler
    });
};

var getMedals = function (response) {  
    console.log(response); // Do so fancy things with medals, everyone loves
};

$.ajax({
    url: 'https://api.batikansenemoglu.com/v1/profile/blueskan',
    success: getProfile,
    error: errorHandler
});

Sanki biraz daha düzen getirmiş gibiyiz aslında fakat sadece fonksiyonların iç içe girmesini engelleyerek okunabilirliği arttırdık şimdi daha modern yöntemlere geçerek callback hell problemimizi kökünden çözelim :)

Promise API

Öncelikle biraz APImizi basit bir örnekle tanıyalım.

var p1 = new Promise( (resolve, reject) => {  
  resolve('Success!');
  // or
  // reject ("Error!");
} );

p1.then( value => {  
  console.log(value); // Success!
}, reason => {
  console.log(reason); // Error!
} );

Promiseler basitçe 4 parçadan oluşmaktadır.

  • Öncelikle bir new Promise diyerek yeni bir promise yaratılır.
  • Bu Promise ya Resolve edilir ve işlem tamamlanır yada Reject edilir.
  • Daha sonra bu Promise'i kullanırken .then fonksiyonu içinde Resolve ve Reject durumlarını handle eden 2 fonksiyon verilir.
  • Eğer herhangi bir exception throw edilirse, .catch fonksiyonu ile yakalanıp handle edilebilir.
  • En önemlisi bir Promise geriye bir Promise return edebilir, buna Promise Chaining denir ve bir sonraki örneklerimizde özellikle bu kullanımını göreceğiz.

Şimdi aşağıda belirteceğim örnekte XMLHttpRequest API'sini $.ajax fonksiyonu ile dolaylı olarak kullanmak yerine Fetch API kullanacağım, kendi içinde zaten Promiseleri kullanan bu yapı ile konuyu daha iyi anlayabileceğiz.

fetch('https://api.batikansenemoglu.com/v1/profile/blueskan').then(function (response) {  
    return response.json(); // Geriye tekrar bir promise döndürüyoruz
}).then(function (data) {
    console.log(data); // handle profile

    return fetch('https://api.batikansenemoglu.com/v1/profile/blueskan/friends'); // Geriye tekrar bir promise döndürüyoruz
}).then(function (response) {
    return response.json(); // Geriye tekrar bir promise döndürüyoruz
}).then(function (data) {
    console.log(data); // handle friends

    return fetch('https://api.batikansenemoglu.com/v1/profile/blueskan/medals'); // Geriye tekrar bir promise döndürüyoruz
}).then(function (response) {
    return response.json(); // Geriye tekrar bir promise döndürüyoruz
}).then(function (data) {
    console.log(data); // handle medals
}).catch(function (err) {
    console.error(err);
});

Gördüğünüz PROMISE Apisini kullanarak daha okunaklı bir kod elde ettik, hatta şu şekilde toplu olarakta çağırabilirdik.

var getProfile = fetch('https://api.batikansenemoglu.com/v1/profile/blueskan').then(function (response) {  
    return response.json(); // Geriye tekrar bir promise döndürüyoruz
});

var getFriends = fetch('https://api.batikansenemoglu.com/v1/profile/blueskan/friends').then(function (response) {  
    return response.json();
});

var getMedals = fetch('https://api.batikansenemoglu.com/v1/profile/blueskan/medals').then(function (response) {  
    return response.json();
});

Promise.all([getProfile, getFriends, getMedals]).then(function (responses) {  
    // responses[0] = getProfile Response
    // responses[1] = getFriends Response
    // responses[2] = getMedals  Response
});

Promise örneklerimizde dikkat etmemiz gereken Network Condition ve Server Load durumları göz önüne alındığında hangi Request'in hangisinden önce biteceğinin garantisini veremeyiz fakat bu yapı sayesinde tüm Requestlerimiz bittiğinde haberimiz oluyor.

Şuan için baya bir yol katettik, şimdi diğer yöntemimize geçip biraz daha güzel bir hale getirelim.

ES6 Generators

https://github.com/tj/co adresinden detayları incelenebilir.

ES7 Async/Await

ES7 ile beraber artık Async/Await kullanarak asenkron işlemlerimizi senkronmuş gibi yönetebiliyoruz.

Bu işlemleri yapacağımız fonksiyona async şeklinde bekliyoruz ve Promise döndüren yerleri await şeklinde işaretleyip cevap bekleyip devam edebiliyoruz.

Bu şekilde asynchronous yapılan çağrılar synchronous gibi davranıyor. Ve göze çok okunaklı bir kod ortaya çıkıyor :)

async function getDetailAsync () {  
    try {
        var profile = await (await fetch('https://api.batikansenemoglu.com/v1/profile/blueskan')).json();
        var friends = await (await fetch('https://api.batikansenemoglu.com/v1/profile/blueskan/friends')).json();
        var medals  = await (await fetch('https://api.batikansenemoglu.com/v1/profile/blueskan/medals')).json();
    } catch (err) {
        console.error(err);
    }

    return {profile: profile, friends: friends, medals: medals};
}

getDetailAsync.then(function (responses) {  
    console.log(responses.profile);
    console.log(responses.friends);
    console.log(responses.medals);
});

Şimdilik bu kadar herkese mutlu günler :)

Batıkan Senemoğlu

Read more posts by this author.

İstanbul, Turkey https://batikansenemoglu.com