Asynch JS - $.Ertelenmenin gücü

Jeremy Chone
Jeremy Chone

Sorunsuz ve duyarlı HTML5 uygulamaları oluşturmanın en önemli yönlerinden biri, uygulamanın veri getirme, işleme, animasyonlar ve kullanıcı arayüzü öğeleri gibi farklı bölümleri arasındaki senkronizasyondur.

Masaüstü veya yerel ortam arasındaki temel fark, tarayıcıların iş parçacığı modeline erişim vermemesi ve kullanıcı arayüzüne (ör. DOM) erişen her şey için tek bir iş parçacığı sağlamasıdır. Bu, kullanıcı arayüzü öğelerine erişen ve bunları değiştiren tüm uygulama mantığının her zaman aynı iş parçacığında olduğu anlamına gelir. Bu nedenle, tüm uygulama çalışma birimlerini mümkün olduğunca küçük ve verimli tutmak ve tarayıcının sunduğu eşzamansız özelliklerden olabildiğince faydalanmak önemlidir.

Tarayıcı Eşzamansız API'leri

Neyse ki Tarayıcılar, yaygın olarak kullanılan XHR (XMLHttpRequest veya "AJAX") API'leri gibi çeşitli eşzamansız API'lerin yanı sıra IndexedDB, SQLite, HTML5 Web çalışanları ve HTML5 GeoLocation API'leri de sağlar. DOM ile ilgili bazı işlemler bile eşzamansız olarak gösterilir (örneğin, migrationEnd etkinlikleri aracılığıyla CSS3 animasyonu).

Tarayıcıların eşzamansız programlamayı uygulama mantığına sunma yöntemi, etkinlikler veya geri çağırma işlevleridir.
Etkinliğe dayalı eşzamansız API'lerde geliştiriciler, belirli bir nesne (ör. HTML Öğesi veya diğer DOM nesneleri) için bir etkinlik işleyici kaydeder ve ardından işlemi çağırır. Tarayıcı, işlemi genellikle farklı bir iş parçacığında gerçekleştirir ve uygun olduğunda ana iş parçacığında etkinliği tetikler.

Örneğin, etkinlik tabanlı eşzamansız bir API olan XHR API'sını kullanan kod aşağıdaki gibi görünür:

// Create the XHR object to do GET to /data resource  
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);

// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
alert("We got data: " + xhr.response);
}
},false)

// perform the work
xhr.send();

CSS3 migrationEnd etkinliği, etkinlik tabanlı eşzamansız API'ye başka bir örnektir.

// get the html element with id 'flyingCar'  
var flyingCarElem = document.getElementById("flyingCar");

// register an event handler 
// ('transitionEnd' for FireFox, 'webkitTransitionEnd' for webkit) 
flyingCarElem.addEventListener("transitionEnd",function(){
// will be called when the transition has finished.
alert("The car arrived");
});

// add the CSS3 class that will trigger the animation
// Note: some browers delegate some transitions to the GPU , but 
//       developer does not and should not have to care about it.
flyingCarElemen.classList.add('makeItFly') 

SQLite ve HTML5 Geolocation gibi diğer tarayıcı API'leri geri çağırmaya dayalıdır. Diğer bir deyişle, geliştirici, bir işlevi bağımsız değişken olarak iletir. Bu işlev, ilgili çözümle temel uygulama tarafından geri çağrılır.

Örneğin, HTML5 Coğrafi Konumu için kod aşağıdaki gibi görünür:

// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){  
        alert('Lat: ' + position.coords.latitude + ' ' +  
                'Lon: ' + position.coords.longitude);  
});  

Bu örnekte, bir yöntem çağırırız ve istenen sonuçla geri çağrılacak bir işlev iletiriz. Böylece tarayıcı bu işlevi eşzamanlı veya eşzamansız olarak uygulayabilir ve uygulama ayrıntılarından bağımsız olarak geliştiriciye tek bir API verebilir.

Uygulamaları Eşzamansız Olarak Hazır Hale Getirme

İyi tasarlanmış uygulamalar, tarayıcının yerleşik eşzamansız API'lerinin yanı sıra alt düzey API'lerini de (özellikle de G/Ç veya yoğun işlemsel işlemler yaptıklarında) eşzamansız API'lerle sunmalıdır. Örneğin, veri almaya yönelik API'ler eşzamansız olmalı ve aşağıdaki gibi GÖRÜNMEMELİDİR:

// WRONG: this will make the UI freeze when getting the data  
var data = getData();
alert("We got data: " + data);

Bu API tasarımı, getData() işlevinin engellenmesini gerektirir. Bu durum, veriler getirilene kadar kullanıcı arayüzünü dondurur. Veriler JavaScript bağlamında yerelse bu sorun olmayabilir. Ancak verilerin ağdan, hatta bir SQLite veya dizin deposundan yerel olarak getirilmesi gerekiyorsa bu durum, kullanıcı deneyimi üzerinde büyük bir etki yaratabilir.

Doğru tasarım, işlenmesi zaman alabilecek tüm uygulama API'lerini proaktif olarak yapmaktır. Eşzamanlı uygulama kodunun eşzamansız olarak yeniden entegre edilmesi zor bir iş olabilir. Bu nedenle, işin başından itibaren eşzamanlı olmayan bir uygulama.

Örneğin, basit getData() API'si şuna benzeyecektir:

getData(function(data){
alert("We got data: " + data);
});

Bu yaklaşımın iyi tarafı, uygulama kullanıcı arayüzü kodunu baştan itibaren eşzamansız merkezli olmaya zorlamasıdır ve temel API'lerin, eşzamansız olmaları gerekip gerekmediğine karar vermelerine olanak tanımasıdır.

Tüm uygulama API'lerinin eşzamansız olması gerekmediğini veya olması gerekmediğini unutmayın. Genel kural olarak, herhangi bir türde G/Ç veya ağır işlemler (15 ms'den uzun sürebilenler) yapan tüm API'lerin, ilk uygulama eşzamanlı olsa bile başlangıçtan itibaren eşzamansız olarak gösterilmesi gerekir.

Hataları İşleme

Eşzamansız programlamanın en önemli noktalarından biri, hataları yönetmek için kullanılan geleneksel dene-yakala yönteminin artık işe yaramamasıdır. Çünkü hatalar genellikle başka bir iş parçacığında meydana gelir. Sonuç olarak, arayan kişinin, işleme sırasında bir sorun oluştuğunda arayanı bilgilendirecek yapılandırılmış bir yöntemi olması gerekir.

Etkinliğe dayalı eşzamansız bir API'de bu, genellikle etkinliği alırken etkinliği veya nesneyi sorgulayan uygulama kodu tarafından gerçekleştirilir. Geri çağırma tabanlı eşzamansız API'ler için en iyi uygulama, bağımsız değişken olarak uygun hata bilgisine sahip bir hata durumunda çağrılacak işlevi alan ikinci bir bağımsız değişkene sahip olmaktır.

getData çağrımız şöyle görünür:

// getData(successFunc,failFunc);  
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});

$.Deated ile birlikte

Yukarıdaki geri çağırma yaklaşımının bir sınırlaması, orta düzeyde gelişmiş senkronizasyon mantığı yazmanın çok zahmetli olabilmesidir.

Örneğin, üçüncü bir API yapmadan önce iki eşzamansız API'nin tamamlanmasını beklemeniz gerekiyorsa kod karmaşıklığı hızla artabilir.

// first do the get data.   
getData(function(data){
// then get the location
getLocation(function(location){
alert("we got data: " + data + " and location: " + location);
},function(ex){
alert("getLocation failed: "  + ex);
});
},function(ex){
alert("getData failed: " + ex);
});

Uygulamanın, uygulamanın birden fazla bölümünden aynı çağrıyı yapması gerektiğinde işler daha da karmaşıklaşabilir. Bunun nedeni, her aramanın bu çok adımlı çağrıları gerçekleştirmesi veya uygulamanın kendi önbelleğe alma mekanizmasını uygulaması gerekmesidir.

Neyse ki, Promises (Java'daki Future'a benzer) adlı nispeten eski bir kalıp; jQuery çekirdeğinde, eşzamansız programlamaya basit ve güçlü bir çözüm sağlayan $.Deated adlı sağlam ve modern bir uygulama vardır.

Süreci basitleştirmek için Promise kalıbı, eşzamansız API'nin "Sonucun karşılık gelen verilerle çözümleneceğine dair taahhüt"e benzeyen bir Promise nesnesi döndürdüğünü tanımlar. Arayan, çözümü elde etmek için Promise nesnesini alır ve "done(successFunc(data))) yöntemini çağırarak Promise nesnesine, "successFunc" çözümlendiğinde bu öğeyi çağırmasını söyler.

Dolayısıyla, yukarıdaki getData çağrı örneği şu şekilde olur:

// get the promise object for this API  
var dataPromise = getData();

// register a function to get called when the data is resolved
dataPromise.done(function(data){
alert("We got data: " + data);
});

// register the failure function
dataPromise.fail(function(ex){
alert("oops, some problem occured: " + ex);
});

// Note: we can have as many dataPromise.done(...) as we want. 
dataPromise.done(function(data){
alert("We asked it twice, we get it twice: " + data);
});

Burada önce dataPromise nesnesini alırız ve ardından veriler çözümlendiğinde geri çağrılmasını istediğimiz bir işlevi kaydetmek için .done yöntemini çağırırız. Nihai hatayı işlemek için .fail yöntemini de çağırabiliriz. Temel Promise uygulaması (jQuery kodu) kaydı ve geri çağırmaları işleyeceği için ihtiyaç duyduğumuz kadar .done veya .fail çağrısına sahip olabiliriz.

Bu kalıpla, daha gelişmiş senkronizasyon kodu uygulamak nispeten kolaydır ve jQuery zaten en yaygın olanı (örneğin, $.when) sağlar.

Örneğin, yukarıdaki iç içe yerleştirilmiş getData/getLocation geri çağırması şuna benzer hale gelir:

// assuming both getData and getLocation return their respective Promise
var combinedPromise = $.when(getData(), getLocation())

// function will be called when both getData and getLocation resolve
combinePromise.done(function(data,location){
alert("We got data: " + dataResult + " and location: " + location);
});  

Tüm bunların güzel yanı, jQuery.Deated'in, geliştiricilerin eşzamansız işlevi uygulamasını son derece kolay hale getirmesidir. Örneğin, getData aşağıdaki gibi görünebilir:

function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();

// ---- AJAX Call ---- //
XMLHttpRequest xhr = new XMLHttpRequest();
xhr.open("GET","data",true);

// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
    // 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)
    deferred.resolve(xhr.response);
}else{
    // 3.2) REJECT the DEFERRED (this will trigger all the fail()...)
    deferred.reject("HTTP error: " + xhr.status);
}
},false) 

// perform the work
xhr.send();
// Note: could and should have used jQuery.ajax. 
// Note: jQuery.ajax return Promise, but it is always a good idea to wrap it
//       with application semantic in another Deferred/Promise  
// ---- /AJAX Call ---- //

// 2) return the promise of this deferred
return deferred.promise();
}

Dolayısıyla, getData() çağrıldığında, çağrıyı yapanın tamamlandı ve başarısız işlevlerini kaydedebilmesi için önce yeni bir jQuery.De bağımsız nesne (1) oluşturur ve daha sonra Promise (2) öğesini döndürür. Daha sonra XHR çağrısı geri döndüğünde, ertelenen durumu (3.1) çözümler veya reddeder (3.2). derated.resolve yapmak, tümDone(...) işlevlerini ve diğer vaat işlevlerini (ör. sonra ve düp) tetikler ve deded.reject çağırmak tüm başarısız() işlevlerini çağırır.

Kullanım Alanları

Erteleme seçeneğinin oldukça faydalı olabileceği bazı iyi kullanım örnekleri aşağıda verilmiştir:

Veri Erişimi: Veri erişimi API'lerini $.Ertelenmiş olarak göstermek genellikle doğru tasarımdır. Eşzamanlı uzaktan çağrılar kullanıcı deneyimini tamamen bozacağından bu durum uzak veriler için barizdir.Ancak yerel veriler için de alt düzey API'ler (ör. SQLite ve IndexedDB) eşzamansız olanlardır. Ertelenmiş API'nin $.when ve .pipe eşzamansız alt sorguları senkronize ve zincirleme olarak son derece etkilidir.

Kullanıcı Arayüzü Animasyonları: Bir veya daha fazla animasyonu migrationEnd etkinlikleriyle düzenlemek, özellikle animasyonlar CSS3 animasyonu ile JavaScript'in bir karışımı olduğunda (genellikle olduğu gibi) oldukça sıkıcı olabilir. Animasyon işlevlerini Ertelenenler olarak sarmalamak, kod karmaşıklığını önemli ölçüde azaltabilir ve esnekliği artırabilir. cssAnimation(className) gibi, migrationEnd'da çözümlenen Promise nesnesini döndürecek basit bir genel sarmalayıcı işlevi bile çok yardımcı olabilir.

Kullanıcı Arayüzü Bileşeni Görüntüleme: Bu, biraz daha gelişmiş bir yöntemdir, ancak gelişmiş HTML Bileşeni çerçeveleri de Ertelenenler'i kullanmalıdır. Bir uygulamanın kullanıcı arayüzünün farklı bölümlerini görüntülemesi gerektiğinde ayrıntılara çok fazla girmeden (bu, başka bir gönderide konu olacaktır). Bu bileşenlerin yaşam döngüsünü Ertelenenler kategorisine dahil etmek, zamanlama üzerinde daha fazla kontrol sahibi olmanızı sağlar.

Herhangi bir tarayıcı eşzamansız API'si: Normalleştirme amacıyla, tarayıcı API çağrılarını Ertelenmiş olarak sarmalamak genellikle iyi bir fikirdir. Bu işlemin her biri 4-5 kod satırı içerse de tüm uygulama kodlarını önemli ölçüde basitleştirir. Yukarıdaki getData/getLocation sözde kodunda gösterildiği gibi bu, uygulama kodunun tüm API türlerinde (tarayıcılar, uygulama özellikleri ve bileşik) tek bir eşzamansız modele sahip olmasını sağlar.

Önbelleğe alma: Bunun bir yan avantajıdır ancak bazı durumlarda çok yararlı olabilir. Promise API'leri (ör. .done(...) ve .fail(...)) eşzamansız çağrı yapılmadan önce veya gerçekleştirildikten sonra çağrılabilir. Ertelenen nesne, eşzamansız bir çağrı için önbelleğe alma işleyicisi olarak kullanılabilir. Örneğin, bir CacheManager belirli istekler için Ertelenenler'i takip edebilir ve geçersiz kılınmadıysa eşleşen Ertelenen Promise değerini döndürebilir. Bunun güzel tarafı, çağrıyı yapanın aramanın zaten çözümlenip çözümlenmediğini veya çözümlenme sürecinde olup olmadığını bilmesine gerek olmamasıdır. Geri çağırma işlevi tam olarak aynı şekilde çağrılır.

Sonuç

$.Ertelenen kavramı basit olsa da bunu iyi bir şekilde kavramak zaman alabilir. Ancak tarayıcı ortamının doğası gereği, ciddi HTML5 uygulama geliştiricileri için JavaScript'te eşzamansız programlamayı ustalıkla öğrenmek gerekir. Promise kalıbı (ve jQuery uygulaması), eşzamansız programlamayı güvenilir ve güçlü hale getiren muazzam araçlardır.