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ü ortamı veya yerel ortamla ilgili temel fark, tarayıcıların iş parçacığı modeline erişim izni 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 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 eşzamansız olarak gösterilir (örneğin, geçişEnd etkinlikleri aracılığıyla CSS3 animasyonları).
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 ileti dizisinde gerçekleştirir ve uygun olduğunda ana ileti dizisinde 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 Geolocation gibi diğer tarayıcı API'leri geri çağırma tabanlıdır. Bu, geliştiricinin temel uygulama tarafından ilgili çözünürlükle geri çağrılacak bir bağımsız değişken olarak iletildiği anlamına gelir.
Ö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 bir şekilde sunmalıdır. Özellikle de herhangi bir G/Ç veya yoğun hesaplama işlemi gerçekleştirirken bu şekilde davranmalıdır. Örneğin, verileri almak için kullanılan API'ler eşzamansız olmalı ve şu şekilde 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 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, işlenmesi zaman alacak tüm uygulama API'lerini proaktif bir şekilde yapmaktır. İşlenmesi biraz zaman alabilir. Eşzamanlı uygulama kodunun eşzamansız olması için geriye dönük düzenleme yapılması göz korkutucu bir görev olabilir.
Ö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. Ayrıca, temel API'lerin daha sonraki bir aşamada eşzamanlı olup olmayacağına karar vermesine olanak tanır.
Tüm uygulama API'lerinin eşzamansız olması gerekmediğini veya olması gerekmediğini unutmayın. 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şlem sırasında bir sorun olduğunda arayanı bilgilendirmek için yapılandırılmış bir yönteme sahip olması gerekir.
Etkinliğe dayalı eşzamansız API'lerde bu işlem, genellikle etkinlik alınırken etkinliği veya nesneyi sorgulayan uygulama kodu tarafından gerçekleştirilir. Geri çağırma tabanlı asenkron API'ler için en iyi uygulama, hata durumunda uygun hata bilgileriyle birlikte çağrılacak bir işlevi 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);
});
$.Derated ile bir araya getiriliyor
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 gelebileceğidir.
Ö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, uygulamanın birden fazla bölümünden aynı çağrıyı yapması gerektiğinde işler daha da karmaşık hale gelebilir. Bunun nedeni, her çağrının bu çok adımlı çağrıları veya uygulamanın kendi önbelleğe alma mekanizmasını uygulaması gerekecektir.
Neyse ki Promises (Java'daki Future'a benzer) adı verilen nispeten eski bir kalıp ve jQuery'nin temelinde, asenkron programlamaya basit ve güçlü bir çözüm sunan $.Deferred adlı sağlam ve modern bir uygulama var.
Bunu basitleştirmek için Promise kalıbı, eşzamansız API'nin bir tür "Sonucun karşılık gelen verilerle çözümleneceğine dair Promise" niteliğinde bir Promise nesnesi döndürdüğünü tanımlar. Çözümü almak için çağrı yapan kişi, Promise nesnesini alır ve done(successFunc(data)) yöntemini çağırarak Promise nesnesine bu nesneye successFunc çağrısı yapması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 kodunun uygulanması nispeten daha kolaydır ve jQuery, zaten en yaygın olanı (ör. $.when) 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.De disapproved nesnesi (1) oluşturur ve ardından Promise (Vadet) (2) işlevini döndürür. Böylece çağrıyı, çağrının tamamlandı olarak kaydedip başarısız olabilir. Ardından, XHR çağrısı geri döndüğünde, ertelenen (3.1) çağrısını çözümler veya reddeder (3.2). deferred.resolve işlevi çağrılırsa tüm done(…) işlevleri ve diğer promise işlevleri (ör. then ve pipe) tetiklenir. deferred.reject işlevi çağrılırsa tüm fail() işlevleri çağrılır.
Kullanım Alanları
Aşağıda, Ertelenenler özelliğinin çok kullanışlı olabileceği bazı iyi kullanım örnekleri verilmiştir:
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ı söyleyebiliriz.
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, uygulama kodunun tüm API türlerinde (tarayıcılar, uygulamaya özgü özellikler ve birleşik) tek bir asenkron modele sahip olmasını sağlar.
Ö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 Ertelenenler'i takip edebilir ve geçersiz kılınmamışsa eşleşen Ertelenen Vaadi'ni 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. Bununla birlikte, tarayıcı ortamının doğası gereği, eşzamansız programlamayı JavaScript'te uzmanlaşmak, ciddi HTML5 uygulama geliştiricileri için bir gerekliliktir. Promise kalıbı (ve jQuery uygulaması) ise eşzamansız programlamayı güvenilir ve güçlü hale getirmek için muazzam araçlardır.