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ümlerinin senkronizasyonudur.
Masaüstü veya yerel ortamla arasındaki temel fark, tarayıcıların mesaj dizi modeli erişimi vermemesi ve kullanıcı arayüzüne (ör. DOM) erişen her şey için tek bir mesaj dizisi 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 iş birimlerini mümkün olduğunca küçük ve verimli tutmanın ve tarayıcının sunduğu tüm asenkron özelliklerden mümkün olduğunca yararlanmanın önemi vardır.
Tarayıcı Asenkron 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'ler ve IndexedDB, SQLite, HTML5 web işçileri ve HTML5 Coğrafi Konum API'leri gibi API'ler sağlar. DOM ile ilgili bazı işlemler bile (ör. transitionEnd etkinlikleri aracılığıyla CSS3 animasyonu) asenkron olarak gösterilir.
Tarayıcılar, eşzamansız programlamayı uygulama mantığına etkinlikler veya geri çağırma işlevleri aracılığıyla sunar.
Etkinlik tabanlı asenkron 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ı bir zaman uyumsuz API olan XHR API'yi kullanan kod şu şekilde 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 transitionEnd etkinliği, olaya dayalı asenkron API'ye örnek olarak verilebilir.
// 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 Coğrafi Konum gibi diğer tarayıcı API'leri geri çağırma tabanlı olduğundan geliştirici, temel uygulama tarafından ilgili çözünürlükle geri çağrılacak bir işlevi bağımsız değişken olarak iletir.
Örneğin, HTML5 Coğrafi Konum için kod şu şekilde 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 durumda, bir yöntemi çağırıp istenen sonuçla geri çağrılacak bir işlev iletmemiz yeterlidir. Bu sayede tarayıcı, bu işlevi senkronize veya asenkron olarak uygulayabilir ve uygulama ayrıntılarından bağımsız olarak geliştiriciye tek bir API sunabilir.
Uygulamaları Eşzamansız Olarak Hazır Hale Getirme
İyi tasarlanmış uygulamalar, tarayıcının yerleşik asenkron API'lerinin yanı sıra düşük seviye API'lerini de asenkron olarak sunmalıdır. Özellikle de herhangi bir G/Ç veya yoğun hesaplama işlemi yaptıklarında bu geçerlidir. Örneğin, veri alacak API'ler asenkron olmalı ve aşağıdaki gibi DEĞİLDİ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 engellemesini gerektirir. Bu durumda, veriler getirilene kadar kullanıcı arayüzü donar. Veriler JavaScript bağlamında yerelse bu sorun oluşturmayabilir. Ancak verilerin ağdan veya hatta yerel olarak bir SQLite ya da dizin deposundan getirilmesi gerekiyorsa bu durum kullanıcı deneyimini önemli ölçüde etkileyebilir.
Doğru tasarım, senkronize uygulama kodunu senkronize olmayan hale getirmek zor bir iş olabileceğinden, işlenmesinin biraz zaman alabileceği tüm uygulama API'lerini baştan senkronize olmayan hale getirmektir.
Örneğin, basit getData() API'si şöyle bir şey olur:
getData(function(data){
alert("We got data: " + data);
});
Bu yaklaşımın avantajı, uygulama kullanıcı arayüzü kodunun baştan eşzamansız odaklı olmasını zorunlu kılmasıdır ve temel API'lerin daha sonraki bir aşamada eşzamanlı olup olmayacağına karar vermesine olanak tanır.
Uygulama API'sinin tümünün asenkron olması gerekmez veya gerekmemelidir. Genel kural, herhangi bir türde G/Ç veya ağır işlem yapan API'lerin (15 ms'den uzun sürebilecek her şey), ilk uygulama senkronize olsa bile baştan itibaren asenkron olarak sunulması gerektiğidir.
Başarısızlıkları işleme
Asenkron programlamanın bir dezavantajı, hataların genellikle başka bir iş parçacığında gerçekleşmesi nedeniyle, hataları ele almanın geleneksel try/catch yönteminin artık pek işe yaramaması. Bu nedenle, aranan tarafın, işleme sırasında bir sorun olduğunda arayanı bilgilendirmek için yapılandırılmış bir yönteme sahip olması gerekir.
Etkinlik tabanlı bir asenkron API'de bu işlem genellikle uygulama kodunun, etkinliği veya nesneyi aldığında etkinliği sorgulamasıyla gerçekleştirilir. Geri çağırma tabanlı asenkron API'ler için en iyi uygulama, hata durumunda uygun hata bilgilerini bağımsız değişken olarak alan bir işlev alan ikinci bir bağımsız değişken kullanmaktı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);
});
$.Deferred ile bir araya getirme
Yukarıdaki geri çağırma yaklaşımının bir sınırlaması, orta düzeyde gelişmiş senkronizasyon mantığı yazmanın bile gerçekten zahmetli hale gelmesidir.
Örneğin, üçüncü bir API'yi kullanmadan ö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 aynı çağrıyı uygulamanın birden fazla bölümünden yapması gerektiğinde işler daha da karmaşık hale gelebilir. Çünkü her çağrının bu çok adımlı çağrıları gerçekleştirmesi veya uygulamanın kendi önbelleğe alma mekanizmasını uygulaması gerekir.
Neyse ki Promises (Java'daki Future'a benzer) adı verilen nispeten eski bir kalıp ve jQuery çekirdeğinde $.Deferred adlı sağlam ve modern bir uygulama var. Bu uygulama, asenkron programlamaya basit ve güçlü bir çözüm sunuyor.
Basitleştirmek gerekirse Promise kalıbı, asenkron API'nin bir Promise nesnesi döndürdüğünü tanımlar. Bu nesne, "sonuç, ilgili verilerle çözülecek" türünde bir "Promise"dir. Çözümü almak için çağıran, Promise nesnesini alır ve done(successFunc(data)) çağrısı yapar. Bu çağrı, Promise nesnesine "data" çözüldüğünde bu successFunc işlevini çağırmasını söyler.
Bu nedenle, yukarıdaki getData çağrısı ö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üldüğünde geri çağrılmasını istediğimiz bir işlevi kaydetmek için .done yöntemini çağırırız. Olası hatayı işlemek için .fail yöntemini de çağırabiliriz. Temel Promise uygulaması (jQuery kodu) kayıt ve geri çağırma işlemlerini yöneteceğinden, istediğimiz kadar .done veya .fail çağrısı yapabileceğimizi unutmayın.
Bu kalıpla daha gelişmiş senkronizasyon kodu uygulamak nispeten kolaydır ve jQuery zaten $.when gibi en yaygın olanı sağlar.
Örneğin, yukarıdaki iç içe yerleştirilmiş getData/getLocation geri çağırma işlevi aşağıdaki gibi olur:
// 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 en güzel yanı, jQuery.Deferred'in geliştiricilerin eşzamansız işlevi uygulamasını çok kolaylaştırmasıdır. Örneğin, getData şu şekilde 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();
}
Bu nedenle, getData() çağrıldığında önce yeni bir jQuery.Deferred nesnesi (1) oluşturur ve ardından arayanın done ve fail işlevlerini kaydedebilmesi için Promise'ini (2) döndürür. Ardından, XHR çağrısı döndürüldüğünde ertelenen çağrıyı çözer (3.1) veya reddeder (3.2). deferred.resolve işlevi tüm done(…) işlevlerini ve diğer promise işlevlerini (ör.then ve pipe) tetikler. deferred.reject işlevi ise tüm fail() işlevlerini çağırır.
Kullanım Alanları
Ertelenen'in çok yararlı olabileceği bazı iyi kullanım alanları şunlardır:
Veri erişimi: Veri erişimi API'lerini $.Deferred olarak göstermek genellikle doğru tasarımdır. Senkronize uzak çağrılar kullanıcı deneyimini tamamen bozacağından bu durum uzak veriler için açıktır ancak genellikle alt düzey API'ler (ör. SQLite ve IndexedDB) kendileri de asenkrondur. Ertelenen API'nin $.when ve .pipe işlevleri, eşzamansız alt sorguları senkronize etmek ve zincirlemek için son derece güçlüdür.
Kullanıcı Arayüzü Animasyonları: transitionEnd etkinlikleriyle bir veya daha fazla animasyonu koordine etmek, özellikle de animasyonlar CSS3 animasyonu ve JavaScript'in bir karışımı olduğunda (genellikle olduğu gibi) oldukça zahmetli olabilir. Animasyon işlevlerini ertelenen olarak sarmalamak, kod karmaşıklığını önemli ölçüde azaltabilir ve esnekliği artırabilir. transitionEnd'de çözülen Promise nesnesini döndürecek cssAnimation(className) gibi basit bir genel sarmalayıcı işlevi bile çok yardımcı olabilir.
Kullanıcı Arayüzü Bileşeni Görüntüleme: Bu yöntem biraz daha gelişmiştir ancak gelişmiş HTML bileşeni çerçeveleri de ertelenen yöntemi kullanmalıdır. Ayrıntılara çok fazla girmeden (bu konu başka bir makalenin konusu olacak), bir uygulamanın kullanıcı arayüzünün farklı bölümlerini göstermesi gerektiğinde, bu bileşenlerin yaşam döngüsünün ertelenen içinde kapsüllenmesinin zamanlama üzerinde daha fazla kontrol sahibi olmanızı sağladığını belirtmek isteriz.
Herhangi bir tarayıcıda asenkron API: Normalleştirme amacıyla, tarayıcı API çağrılarını genellikle ertelenmiş olarak sarmalamak iyi bir fikirdir. Bu işlem her biri için tam anlamıyla 4-5 satır kod gerektirir ancak tüm uygulama kodlarını büyük ölçüde basitleştirir. Yukarıdaki getData/getLocation sözde kodunda gösterildiği gibi, bu durum uygulama kodunun tüm API türlerinde (tarayıcılar, uygulamaya özgü özellikler ve birleşik) tek bir asenkron modele sahip olmasına olanak tanır.
Önbelleğe alma: Bu, yan bir avantajdır ancak bazı durumlarda çok yararlı olabilir. Promise API'leri (ör. .done(…) ve .fail(…)) çağrılabilir. Deferred nesnesi, asenkron çağrı için bir önbelleğe alma işleyicisi olarak kullanılabilir. Örneğin, bir CacheManager, belirli istekler için ertelenenleri takip edebilir ve geçersiz kılınmamışsa eşleşen ertelenenin Promise'ini döndürebilir. Güzel olan, arayanın çağrının çözülüp çözülmediğini veya çözülmekte olup olmadığını bilmesi gerekmemesidir. Geri çağırma işlevi tam olarak aynı şekilde çağrılır.
Sonuç
$.Deferred kavramı basit olsa da bu kavramı iyice anlamak zaman alabilir. Ancak tarayıcı ortamının doğası gereği, JavaScript'te eşzamansız programlamaya hakim olmak ciddi bir HTML5 uygulama geliştiricisi için şarttır ve Promise kalıbı (ve jQuery uygulaması), eşzamansız programlamayı güvenilir ve güçlü hale getirmek için mükemmel araçlardır.