Salah satu aspek terpenting dalam mem-build aplikasi HTML5 yang lancar dan responsif adalah sinkronisasi antara semua bagian aplikasi yang berbeda seperti pengambilan, pemrosesan, animasi, dan elemen antarmuka pengguna data.
Perbedaan utamanya dengan desktop atau lingkungan native adalah browser tidak memberikan akses ke model threading dan menyediakan satu thread untuk semua yang mengakses antarmuka pengguna (yaitu DOM). Artinya, semua logika aplikasi yang mengakses dan mengubah elemen antarmuka pengguna selalu berada di thread yang sama, sehingga penting untuk menjaga semua unit kerja aplikasi sekecil dan seefisien mungkin serta memanfaatkan kemampuan asinkron yang ditawarkan browser sebanyak mungkin.
API Asinkron Browser
Untungnya, Browser menyediakan sejumlah API asinkron seperti API XHR (XMLHttpRequest atau 'AJAX') yang biasa digunakan, serta IndexedDB, SQLite, pekerja Web HTML5, dan HTML5 GeoLocation API untuk beberapa nama. Bahkan beberapa tindakan terkait DOM ditampilkan secara asinkron, seperti animasi CSS3 melalui peristiwa transitionEnd.
Cara browser mengekspos pemrograman asinkron ke logika aplikasi adalah melalui peristiwa atau callback.
Dalam API asinkron berbasis peristiwa, developer mendaftarkan pengendali peristiwa untuk objek tertentu (misalnya Elemen HTML atau objek DOM lainnya), lalu memanggil tindakan. Browser akan melakukan
tindakan biasanya di thread yang berbeda, dan memicu peristiwa di thread utama jika sesuai.
Misalnya, kode yang menggunakan XHR API, API asinkron berbasis peristiwa, akan terlihat seperti ini:
// 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();
Peristiwa transitionEnd CSS3 adalah contoh lain dari API asinkron berbasis peristiwa.
// 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')
API browser lainnya, seperti SQLite dan Geolokasi HTML5, berbasis callback, yang berarti developer meneruskan fungsi sebagai argumen yang akan dipanggil kembali oleh implementasi yang mendasarinya dengan resolusi yang sesuai.
Misalnya, untuk Geolokasi HTML5, kodenya akan terlihat seperti ini:
// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){
alert('Lat: ' + position.coords.latitude + ' ' +
'Lon: ' + position.coords.longitude);
});
Dalam hal ini, kita hanya memanggil metode dan meneruskan fungsi yang akan dipanggil kembali dengan hasil yang diminta. Cara ini memungkinkan browser menerapkan fungsi ini secara sinkron atau asinkron dan memberikan satu API kepada developer, terlepas dari detail implementasinya.
Membuat Aplikasi Siap Asinkron
Selain API asinkron bawaan browser, aplikasi yang dirancang dengan baik juga harus mengekspos API tingkat rendahnya secara asinkron, terutama saat melakukan segala jenis I/O atau pemrosesan berat komputasi. Misalnya, API untuk mendapatkan data harus asinkron, dan TIDAK boleh terlihat seperti ini:
// WRONG: this will make the UI freeze when getting the data
var data = getData();
alert("We got data: " + data);
Desain API ini mengharuskan getData() diblokir, yang akan membekukan antarmuka pengguna hingga data diambil. Jika data bersifat lokal dalam konteks JavaScript, hal ini mungkin bukan masalah, tetapi jika data perlu diambil dari jaringan atau bahkan secara lokal di penyimpanan SQLite atau indeks, hal ini dapat berdampak drastis pada pengalaman pengguna.
Desain yang tepat adalah secara proaktif membuat semua API aplikasi yang dapat memerlukan waktu untuk diproses, asinkron sejak awal karena perkuatan kode aplikasi sinkron menjadi asinkron bisa menjadi tugas yang menyulitkan.
Misalnya, API getData() yang sederhana akan menjadi seperti ini:
getData(function(data){
alert("We got data: " + data);
});
Kelebihan dari pendekatan ini adalah kode UI aplikasi akan berfokus pada asinkron sejak awal dan memungkinkan API yang mendasarinya untuk memutuskan apakah kode tersebut harus asinkron atau tidak pada tahap selanjutnya.
Perhatikan bahwa tidak semua API aplikasi perlu atau harus asinkron. Aturan umum adalah bahwa API apa pun yang melakukan jenis I/O atau pemrosesan berat (apa pun yang dapat memerlukan waktu lebih dari 15 md) harus diekspos secara asinkron sejak awal meskipun implementasi pertama bersifat sinkron.
Menangani Kegagalan
Salah satu kendala pemrograman asinkron adalah cara try/catch tradisional untuk menangani kegagalan tidak benar-benar berfungsi lagi, karena error biasanya terjadi di thread lain. Akibatnya, pemanggil harus memiliki cara terstruktur untuk memberi tahu pemanggil saat terjadi masalah selama pemrosesan.
Dalam API asinkron berbasis peristiwa, hal ini sering kali dilakukan oleh kode aplikasi yang membuat kueri peristiwa atau objek saat menerima peristiwa. Untuk API asinkron berbasis callback, praktik terbaiknya adalah memiliki argumen kedua yang menggunakan fungsi yang akan dipanggil jika terjadi kegagalan dengan informasi error yang sesuai sebagai argumen.
Panggilan getData akan terlihat seperti ini:
// getData(successFunc,failFunc);
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});
Menggabungkannya dengan $.Deferred
Salah satu batasan pendekatan callback di atas adalah bahwa pendekatan ini dapat menjadi sangat rumit untuk menulis logika sinkronisasi yang cukup canggih.
Misalnya, jika Anda perlu menunggu dua API asinkron selesai sebelum melakukan API ketiga, kompleksitas kode dapat meningkat dengan cepat.
// 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);
});
Hal-hal bahkan bisa menjadi lebih rumit saat aplikasi perlu melakukan panggilan yang sama dari beberapa bagian aplikasi, karena setiap panggilan harus melakukan panggilan multi langkah ini, atau aplikasi harus menerapkan mekanisme penyimpanan dalam cache-nya sendiri.
Untungnya, ada pola yang relatif lama, yang disebut Promise (mirip dengan Future di Java) dan implementasi yang andal dan modern di jQuery core yang disebut $.Deferred yang memberikan solusi sederhana dan canggih untuk pemrograman asinkron.
Sederhananya, pola Promise menentukan bahwa API asinkron menampilkan objek Promise yang merupakan semacam "Promise bahwa hasil akan di-resolve dengan data yang sesuai". Untuk mendapatkan resolusi, pemanggil akan mendapatkan objek Promise dan memanggil done(SuccessFunc(data)) yang akan memberi tahu objek Promise untuk memanggil berhasilFunc ini jika “data” diselesaikan.
Jadi, contoh panggilan getData di atas menjadi seperti ini:
// 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);
});
Di sini, kita dapatkan objek dataPromise terlebih dahulu, lalu panggil metode .done untuk mendaftarkan fungsi yang ingin dipanggil kembali saat data diselesaikan. Kita juga bisa memanggil metode .fail untuk menangani kegagalan. Perhatikan bahwa kita dapat memiliki panggilan .done atau .fail sebanyak yang diperlukan karena implementasi Promise yang mendasarinya (kode jQuery) akan menangani pendaftaran dan callback.
Dengan pola ini, relatif mudah untuk menerapkan kode sinkronisasi yang lebih canggih, dan jQuery sudah menyediakan kode yang paling umum seperti $.when.
Misalnya, callback getData/getLocation bertingkat di atas akan menjadi seperti ini:
// 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);
});
Dan kelebihan dari itu semua adalah jQuery.Deferred sangat memudahkan developer untuk menerapkan fungsi asinkron. Misalnya, getData dapat terlihat seperti ini:
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();
}
Jadi, saat getData() dipanggil, pertama-tama akan membuat objek jQuery.Deferred baru (1) lalu menampilkan Promise-nya (2) sehingga pemanggil dapat mendaftarkan fungsi selesai dan gagalnya. Kemudian, ketika panggilan XHR kembali, panggilan akan menyelesaikan yang ditangguhkan (3.1) atau menolak (3.2). Melakukan deferred.resolve akan memicu semua fungsi done(…) dan fungsi promise lainnya (misalnya, then dan pipe) dan memanggil deferred.reject akan memanggil semua fungsi fail().
Kasus Penggunaan
Berikut beberapa kasus penggunaan yang baik saat Deferred dapat sangat berguna:
Akses Data: Mengekspos API akses data sebagai $.Deferred sering kali merupakan desain yang tepat. Hal ini jelas untuk data jarak jauh, karena panggilan jarak jauh sinkron akan benar-benar merusak pengalaman pengguna, tetapi juga berlaku untuk data lokal karena sering kali API level yang lebih rendah (misalnya, SQLite dan IndexedDB) bersifat asinkron. $.when dan .pipe pada Deferred API sangat andal untuk menyinkronkan dan merangkai sub-kueri asinkron.
Animasi UI: Mengatur satu atau beberapa animasi dengan peristiwa transitionEnd dapat cukup merepotkan, terutama jika animasi tersebut merupakan campuran animasi CSS3 dan JavaScript (seperti yang sering terjadi). Menggabungkan fungsi animasi sebagai Ditangguhkan dapat secara signifikan mengurangi kompleksitas kode dan meningkatkan fleksibilitas. Bahkan fungsi wrapper umum sederhana seperti cssAnimation(className) yang akan menampilkan objek Promise yang di-resolve pada transisiEnd dapat membantu.
Tampilan Komponen UI: Ini sedikit lebih canggih, tetapi framework Komponen HTML lanjutan juga harus menggunakan Deferred. Tanpa terlalu membahas detailnya (ini akan menjadi subjek postingan lain), saat aplikasi perlu menampilkan berbagai bagian antarmuka pengguna, memiliki siklus proses komponen tersebut yang dienkapsulasi dalam Deferred memungkinkan kontrol waktu yang lebih besar.
API asinkron browser apa pun: Untuk tujuan normalisasi, sebaiknya gabungkan panggilan API browser sebagai Deferred. Ini memerlukan 4 hingga 5 baris kode, tetapi akan sangat menyederhanakan kode aplikasi. Seperti yang ditampilkan dalam kode pseudo getData/getLocation di atas, hal ini memungkinkan kode aplikasi memiliki satu model asinkron di semua jenis API (browser, spesifikasi aplikasi, dan senyawa).
Cache: Ini adalah manfaat sampingan, tetapi dapat sangat berguna dalam beberapa kasus. Karena Promise API (misalnya, .done(…) dan .fail(…)) dapat dipanggil sebelum atau setelah panggilan asinkron dilakukan, objek Deferred dapat digunakan sebagai handle penyimpanan dalam cache untuk panggilan asinkron. Misalnya, CacheManager dapat melacak Ditangguhkan untuk permintaan tertentu, dan menampilkan Promise dari Ditangguhkan yang cocok jika tidak dibatalkan. Kelebihannya adalah pemanggil tidak perlu mengetahui apakah panggilan telah diselesaikan atau sedang dalam proses diselesaikan, fungsi callback-nya akan dipanggil dengan cara yang sama persis.
Kesimpulan
Meskipun konsep $.Deferred sederhana, perlu waktu untuk memahaminya dengan baik. Namun, mengingat sifat lingkungan browser, menguasai pemrograman asinkron di JavaScript adalah suatu keharusan bagi developer aplikasi HTML5 yang serius dan pola Promise (dan implementasi jQuery) adalah alat yang luar biasa untuk membuat pemrograman asinkron menjadi andal dan canggih.