Asynch JS - Kemampuan $.Deferred

Salah satu aspek terpenting tentang pembuatan aplikasi HTML5 yang mulus dan responsif adalah sinkronisasi antara semua bagian aplikasi yang berbeda seperti pengambilan data, pemrosesan, animasi, dan elemen antarmuka pengguna.

Perbedaan utama dengan lingkungan native atau desktop adalah browser tidak memberikan akses ke model threading dan menyediakan satu thread untuk semua hal yang mengakses antarmuka pengguna (yaitu DOM). Artinya, semua logika aplikasi yang mengakses dan mengubah elemen antarmuka pengguna selalu berada di thread yang sama. Oleh karena itu, penting untuk menjaga semua unit kerja aplikasi sekecil dan seefisien mungkin, serta memanfaatkan semua kemampuan asinkron yang ditawarkan browser.

API Asinkron Browser

Untungnya, Browser menyediakan sejumlah API asinkron seperti API XHR (XMLHttpRequest atau 'AJAX') yang umum digunakan, serta IndexedDB, SQLite, pekerja Web HTML5, dan API GeoLocation HTML5. Bahkan beberapa tindakan terkait DOM ditampilkan secara asinkron, animasi CSS3 tersebut melalui peristiwa transisiEnd.

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 ini biasanya dalam 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 transisiEnd 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 cukup memanggil metode dan meneruskan fungsi yang akan dipanggil kembali dengan hasil yang diminta. Hal ini memungkinkan browser untuk mengimplementasikan 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 didesain dengan baik juga harus mengekspos API level rendah secara asinkron, terutama saat aplikasi tersebut melakukan semacam I/O atau pemrosesan komputasi yang berat. Misalnya, API untuk mendapatkan data harus bersifat 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 pemblokiran getData(), yang akan membekukan antarmuka pengguna hingga data diambil. Jika data bersifat lokal dalam konteks JavaScript, hal ini mungkin tidak menjadi masalah. Namun, jika data perlu diambil dari jaringan atau bahkan secara lokal di SQLite atau penyimpanan indeks, hal ini dapat menimbulkan dampak yang signifikan pada pengalaman pengguna.

Desain yang tepat adalah secara proaktif membuat semua API aplikasi yang memerlukan waktu lama untuk diproses, asinkron sejak awal karena penguatan kode aplikasi sinkron menjadi asinkron bisa menjadi tugas yang berat.

Misalnya, getData() API sederhana akan menjadi seperti ini:

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

Hal yang menarik dari pendekatan ini adalah bahwa hal ini memaksa kode UI aplikasi menjadi berpusat secara asinkron dari awal dan memungkinkan API yang mendasarinya memutuskan apakah API tersebut harus asinkron atau tidak di tahap berikutnya.

Perhatikan bahwa tidak semua API aplikasi memerlukan atau harus asinkron. Aturan praktisnya adalah setiap API yang melakukan jenis I/O atau pemrosesan berat (apa pun yang memerlukan waktu lebih dari 15 milidetik) harus diekspos secara asinkron sejak awal, meskipun implementasi pertama dilakukan secara sinkron.

Menangani Kegagalan

Salah satu hasil pemrograman asinkron adalah cara try/catch tradisional untuk menangani kegagalan tidak benar-benar berfungsi lagi, karena error biasanya terjadi di thread lain. Akibatnya, tujuan panggilan harus memiliki cara yang terstruktur untuk memberi tahu pemanggil jika terjadi kesalahan selama pemrosesan.

Pada API asinkron berbasis peristiwa, hal ini sering kali dilakukan dengan kode aplikasi yang membuat kueri untuk peristiwa atau objek saat menerima peristiwa tersebut. 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 keterbatasan pendekatan callback di atas adalah bahwa menulis logika sinkronisasi yang cukup canggih pun dapat menjadi sangat rumit.

Misalnya, jika Anda harus menunggu dua API asinkron selesai dilakukan sebelum melakukan yang 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 kompleks ketika aplikasi perlu melakukan panggilan yang sama dari beberapa bagian aplikasi, karena setiap panggilan harus melakukan panggilan multi langkah ini, atau aplikasi harus mengimplementasikan mekanisme caching-nya sendiri.

Untungnya, ada pola yang relatif lama, yang disebut Promise (jenis mirip dengan Future di Java) dan implementasi yang andal dan modern dalam inti jQuery yang disebut $.Deferred, yang menyediakan solusi sederhana dan canggih untuk pemrograman asinkron.

Sederhananya, pola Promise menetapkan bahwa API asinkron menampilkan objek Promise yang semacam "Promise bahwa hasilnya akan diselesaikan dengan data yang sesuai". Untuk mendapatkan resolusi, pemanggil akan mendapatkan objek Promise dan memanggil done(berhasilFunc(data)) yang akan memberi tahu objek Promise untuk memanggil berhasilFunc jika "data" telah 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 mendapatkan objek dataPromise terlebih dahulu, kemudian memanggil metode .done untuk mendaftarkan fungsi yang ingin dipanggil kembali saat data diselesaikan. Kita juga bisa memanggil metode .fail untuk menangani kegagalan. Perlu diperhatikan bahwa kita dapat melakukan panggilan .done atau .fail sebanyak yang dibutuhkan, 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:

// 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 keunggulan dari semua itu adalah jQuery.Deferred memudahkan developer untuk mengimplementasikan 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, ketika getData() dipanggil, pertama-tama ia akan membuat objek jQuery.Deferred baru (1), lalu menampilkan Promise (2) sehingga pemanggil dapat mendaftarkan fungsi yang selesai dan gagal. Kemudian, ketika panggilan XHR kembali, panggilan ini akan menyelesaikan penangguhan (3.1) atau menolak (3.2). Melakukan menangguhkan.resolve akan memicu semua fungsi yang telah selesai(...) dan fungsi promise lainnya (misalnya, kemudian dan pipa), dan memanggil tugas yang menangguhkan.reject akan memanggil semua fungsi yang gagal().

Kasus Penggunaan

Berikut adalah beberapa kasus penggunaan yang baik saat Ditangguhkan bisa sangat berguna:

Akses Data: Mengekspos API akses data sebagai $.Deferred sering kali merupakan desain yang tepat. Hal ini sudah jelas untuk data jarak jauh, karena panggilan jarak jauh sinkron akan benar-benar merusak pengalaman pengguna, tetapi juga berlaku untuk data lokal seperti API dengan level yang lebih rendah (mis., SQLite dan IndexedDB) bersifat asinkron. Deferred API $.when dan .pipe sangat andal untuk menyinkronkan dan merangkai sub-kueri asinkron.

Animasi UI: Mengorkestrasi satu atau beberapa animasi dengan peristiwa transisiEnd bisa sangat membosankan, terutama jika animasinya 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 diselesaikan pada transisiEnd dapat membantu.

Tampilan Komponen UI: Cara ini sedikit lebih canggih, tetapi framework Komponen HTML lanjutan juga harus menggunakan Ditangguhkan. Tanpa terlalu banyak membahas detail (ini akan menjadi subjek postingan lain), saat aplikasi perlu menampilkan berbagai bagian antarmuka pengguna, siklus proses komponen tersebut yang dienkapsulasi dalam Deferred memungkinkan kontrol waktu yang lebih besar.

Semua API asinkron browser: Untuk tujuan normalisasi, sebaiknya gabungkan panggilan API browser sebagai Ditangguhkan. Setiap langkah ini memerlukan 4 hingga 5 baris kode, tetapi akan sangat menyederhanakan kode aplikasi apa pun. Seperti yang ditunjukkan pada kode pseudo getData/getLocation di atas, hal ini memungkinkan kode aplikasi memiliki satu model asinkron di semua jenis API (browser, spesifikasi aplikasi, dan compound).

Menyimpan ke cache: Ini adalah manfaat sampingan, tetapi bisa sangat berguna dalam beberapa acara. Karena Promise API (misalnya, .done(...) dan .fail(...)) dapat dipanggil sebelum atau setelah panggilan asinkron dilakukan, objek yang Ditangguhkan dapat digunakan sebagai handle caching untuk panggilan asinkron. Misalnya, CacheManager dapat melacak hanya Ditangguhkan untuk permintaan tertentu, dan menampilkan Promise yang Ditangguhkan yang cocok jika belum dibatalkan. Kelebihannya adalah pemanggil tidak harus tahu apakah panggilan telah diselesaikan atau sedang dalam proses diselesaikan, fungsi callback-nya akan dipanggil dengan cara yang sama persis.

Kesimpulan

Meskipun konsep $.Ditangguhkan itu sederhana, perlu waktu untuk memahaminya dengan baik. Namun, mengingat sifat lingkungan browser, penguasaan pemrograman asinkron dalam JavaScript adalah suatu keharusan bagi developer aplikasi HTML5 serius, dan pola Promise (dan implementasi jQuery) adalah alat yang luar biasa untuk membuat pemrograman asinkron andal dan canggih.