Asynch JS - $.Deferred की शक्ति

जेरेमी चॉन
जेरेमी चॉन

आसान और रिस्पॉन्सिव HTML5 ऐप्लिकेशन बनाने के बारे में सबसे अहम पहलू यह है कि ऐप्लिकेशन के अलग-अलग हिस्सों के बीच सिंक किया जाए. जैसे, डेटा फ़ेच करना, प्रोसेस करना, ऐनिमेशन, और यूज़र इंटरफ़ेस एलिमेंट.

डेस्कटॉप या नेटिव एनवायरमेंट में मुख्य अंतर यह है कि ब्राउज़र, थ्रेडिंग मॉडल का ऐक्सेस नहीं देते. साथ ही, यूज़र इंटरफ़ेस (जैसे, डीओएम) को ऐक्सेस करने वाली हर चीज़ के लिए एक ही थ्रेड उपलब्ध कराते हैं. इसका मतलब है कि यूज़र इंटरफ़ेस एलिमेंट को ऐक्सेस करने और उनमें बदलाव करने वाला सभी ऐप्लिकेशन लॉजिक हमेशा एक ही थ्रेड में होते हैं. इसलिए, ऐप्लिकेशन के काम करने वाली सभी यूनिट को जितना हो सके उतना छोटा और असरदार रखें. साथ ही, ब्राउज़र की ओर से ऑफ़र की जाने वाली किसी भी एसिंक्रोनस सुविधा का फ़ायदा लें.

ब्राउज़र एसिंक्रोनस एपीआई

अच्छी बात यह है कि ब्राउज़र कई एसिंक्रोनस एपीआई उपलब्ध कराते हैं, जैसे कि आम तौर पर इस्तेमाल किए जाने वाले XHR (XMLHttpRequest या 'AJAX') एपीआई के साथ-साथ IndexedDB, SQLite, HTML5 Web वर्कर, और HTML5 GeoLocation API. यहां तक कि डीओएम से जुड़ी कुछ कार्रवाइयों को एसिंक्रोनस तरीके से दिखाया जाता है, जैसे कि ट्रांज़िशनEnd इवेंट के ज़रिए CSS3 ऐनिमेशन.

ब्राउज़र, ऐप्लिकेशन लॉजिक के साथ एसिंक्रोनस प्रोग्रामिंग को जिस तरह से दिखाते हैं वह इवेंट या कॉलबैक के ज़रिए होता है.
इवेंट पर आधारित एसिंक्रोनस एपीआई में, डेवलपर किसी ऑब्जेक्ट (जैसे कि एचटीएमएल एलिमेंट या दूसरे डीओएम ऑब्जेक्ट) के लिए, एक इवेंट हैंडलर रजिस्टर करते हैं और फिर कार्रवाई को कॉल करते हैं. ब्राउज़र आम तौर पर, किसी दूसरी थ्रेड में कार्रवाई करेगा. साथ ही, ज़रूरत पड़ने पर मुख्य थ्रेड में इवेंट ट्रिगर करेगा.

उदाहरण के लिए, इवेंट पर आधारित एसिंक्रोनस एपीआई, XHR API का इस्तेमाल करने वाला कोड ऐसा दिखेगा:

// 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 ट्रांज़िशनEnd इवेंट, इवेंट पर आधारित एसिंक्रोनस एपीआई का एक और उदाहरण है.

// 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 और HTML5 जियोलोकेशन जैसे अन्य ब्राउज़र एपीआई कॉलबैक पर आधारित होते हैं. इसका मतलब है कि डेवलपर किसी फ़ंक्शन को आर्ग्युमेंट के तौर पर पास करता है. इसे फ़ंक्शन से जुड़े रिज़ॉल्यूशन के साथ, लागू करने पर वापस कॉल किया जाता है.

उदाहरण के लिए, HTML5 जियोलोकेशन के लिए कोड ऐसा दिखता है:

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

इस मामले में, हम सिर्फ़ एक तरीके को कॉल करते हैं और एक फ़ंक्शन पास करते हैं. इसे वापस अनुरोध किए गए नतीजे के साथ कॉल किया जाएगा. इससे ब्राउज़र, इस फ़ंक्शन को सिंक्रोनस या एसिंक्रोनस तरीके से लागू कर सकता है. साथ ही, लागू करने की जानकारी पर ध्यान दिए बिना, डेवलपर को एक एपीआई दे सकता है.

ऐप्लिकेशन को एसिंक्रोनस-रेडी बनाना

ब्राउज़र में पहले से मौजूद एसिंक्रोनस एपीआई के अलावा, अच्छी तरह से डिज़ाइन किए गए ऐप्लिकेशन को अपने लो लेवल एपीआई को एसिंक्रोनस तरीके से भी दिखाना चाहिए. खास तौर पर तब, जब वे किसी भी तरह का I/O या कंप्यूटेशनल हैवी प्रोसेसिंग करते हैं. उदाहरण के लिए, डेटा पाने के लिए एपीआई एसिंक्रोनस होने चाहिए और उन्हें इस तरह नहीं दिखना चाहिए:

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

इस एपीआई डिज़ाइन के लिए getData() को ब्लॉक करना ज़रूरी है. यह डेटा फ़ेच होने तक उपयोगकर्ता इंटरफ़ेस को फ़्रीज़ कर देगा. अगर JavaScript के हिसाब से डेटा लोकल है, तो हो सकता है कि कोई समस्या न हो. हालांकि, अगर डेटा को नेटवर्क से या स्थानीय रूप से SQLite या इंडेक्स स्टोर से फ़ेच करने की ज़रूरत पड़ती है, तो उपयोगकर्ता अनुभव पर बहुत ज़्यादा असर पड़ सकता है.

सही डिज़ाइन यह है कि जिन ऐप्लिकेशन एपीआई को प्रोसेस होने में थोड़ा समय लग सकता है उन्हें शुरुआत से एसिंक्रोनस बना दिया जाए, क्योंकि सिंक्रोनस ऐप्लिकेशन कोड को एसिंक्रोनस होना एक मुश्किल काम हो सकता है.

उदाहरण के लिए सिंप्लिस्टिक getData() एपीआई कुछ ऐसा बन जाएगा:

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

इस तरीके की सबसे अच्छी बात यह है कि यह ऐप्लिकेशन के यूज़र इंटरफ़ेस (यूआई) कोड को शुरुआत से ही एसिंक्रोनस-आधारित होने के लिए मजबूर करता है. साथ ही, यह दिए गए एपीआई को यह तय करने देता है कि उन्हें एसिंक्रोनस होने की ज़रूरत है या नहीं.

ध्यान दें कि सभी ऐप्लिकेशन एपीआई को ज़रूरी नहीं है या यह एसिंक्रोनस होना चाहिए. थंब का नियम यह है कि किसी भी तरह का I/O या बहुत ज़्यादा प्रोसेसिंग (जिसमें 15 मि॰से॰ से ज़्यादा समय लग सकता है) किसी भी एपीआई को एसिंक्रोनस तरीके से शुरू में दिखाया जाना चाहिए, चाहे पहला लागू सिंक्रोनस हो.

हैंडलिंग के दौरान होने वाली गड़बड़ियां

एसिंक्रोनस प्रोग्रामिंग का एक पहलू यह है कि गड़बड़ियों को हैंडल करने का पारंपरिक 'कोशिश करें/कैच करें' तरीका अब काम नहीं करता, क्योंकि आम तौर पर गड़बड़ियां दूसरे थ्रेड में होती हैं. ऐसे में, कॉल करने वाले व्यक्ति को एक व्यवस्थित तरीका चाहिए, ताकि प्रोसेसिंग के दौरान कोई गड़बड़ी होने पर कॉलर को सूचना दी जा सके.

इवेंट पर आधारित एसिंक्रोनस एपीआई में, अक्सर यह ऐप्लिकेशन कोड से पूरा किया जाता है. यह इवेंट मिलने के दौरान, इवेंट या ऑब्जेक्ट के लिए क्वेरी करता है. कॉलबैक आधारित एसिंक्रोनस एपीआई के लिए, सबसे अच्छा तरीका यह है कि दूसरा आर्ग्युमेंट बनाया जाए. यह एक फ़ंक्शन होता है. इसे आर्ग्युमेंट के तौर पर, गड़बड़ी की सही जानकारी के साथ फ़ेल होने पर कहा जाएगा.

हमारा getData कॉल ऐसा दिखेगा:

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

इसे $.DeTerm की मदद से एक साथ रखा जा रहा है

ऊपर दिए गए कॉलबैक प्रोसेस की एक सीमा यह है कि थोड़ा-बहुत ऐडवांस सिंकिंग लॉजिक लिखना, सच में मुश्किल हो सकता है.

उदाहरण के लिए, अगर आपको कोई तीसरा काम करने से पहले, दो एसिंक्रोनस एपीआई के पूरा होने का इंतज़ार करना है, तो कोड की जटिलता बढ़ सकती है.

// 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);
});

जब ऐप्लिकेशन को ऐप्लिकेशन के कई हिस्सों से एक ही कॉल करने की ज़रूरत होती है, तब चीज़ें और भी जटिल हो सकती हैं. ऐसा इसलिए, क्योंकि हर कॉल को कई चरणों वाले कॉल करने होंगे या ऐप्लिकेशन को कैश मेमोरी में अपना खुद का सिस्टम लागू करना होगा.

अच्छी बात यह है कि इसमें एक पुराना पैटर्न है, जिसे प्रॉमिस (जावा के फ़्यूचर की तरह) कहा जाता है. साथ ही, jQuery कोर में $.Deferred नाम से बेहतर और आधुनिक तरीके से लागू किया जा सकता है, जो एसिंक्रोनस प्रोग्रामिंग का आसान और बेहतरीन समाधान देता है.

इसे आसान बनाने के लिए, प्रॉमिस पैटर्न तय करता है कि एसिंक्रोनस एपीआई एक प्रॉमिस ऑब्जेक्ट दिखाता है जो एक तरह का "प्रॉमिस किया गया कि नतीजे को उससे जुड़े डेटा के साथ रिज़ॉल्व किया जाएगा." रिज़ॉल्यूशन पाने के लिए, कॉलर को प्रॉमिस ऑब्जेक्ट मिलता है और done( हैंडलिंग डेटा) को कॉल किया जाता है. इससे, प्रॉमिस ऑब्जेक्ट को "डेटा" का रिज़ॉल्व होने पर सफलता" कॉल करने के लिए कहा जाएगा.

इसलिए, ऊपर दिया गया getData कॉल उदाहरण कुछ ऐसा होता है:

// 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);
});

यहां हम पहले dataPromise ऑब्जेक्ट लेते हैं और फिर .done तरीके को कॉल करके कोई फ़ंक्शन रजिस्टर करते हैं, जिसे हम डेटा रिज़ॉल्व होने पर वापस कॉल करना चाहते हैं. संभावित गड़बड़ी को ठीक करने के लिए, हम .fail तरीके को भी कॉल कर सकते हैं. ध्यान दें कि हम जितने चाहें उतने .done या .fail कॉल कर सकते हैं. ऐसा इसलिए, क्योंकि पहले से मौजूद प्रॉमिस इंप्लीमेंटेशन (jQuery कोड) रजिस्ट्रेशन और कॉलबैक को हैंडल करेगा.

इस पैटर्न का इस्तेमाल करके, बेहतर सिंक कोड को लागू करना काफ़ी आसान होता है. साथ ही, jQuery पहले से ही सबसे सामान्य कोड, $.when पर लागू करता है.

उदाहरण के लिए, ऊपर दिया गया getData/getLocation कॉलबैक कुछ इस तरह दिखेगा:

// 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);
});  

सबसे अच्छी बात यह है कि jQuery.Deferred की मदद से, डेवलपर आसानी से एसिंक्रोनस फ़ंक्शन लागू कर सकते हैं. उदाहरण के लिए, getData कुछ ऐसा दिख सकता है:

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();
}

इसलिए, getData() को कॉल करने पर, यह सबसे पहले एक नया jQuery.Deferred ऑब्जेक्ट (1) बनाता है और फिर अपना Promise (2) दिखाता है, ताकि कॉलर अपने 'हो गया' और फ़ेल फ़ंक्शन को रजिस्टर कर सके. इसके बाद, जब XHR कॉल वापस आता है, तो यह स्थगित (3.1) या इसे अस्वीकार (3.2) करता है. deced.resolve को लागू करने से, सभी हो गया(...) फ़ंक्शन और दूसरे प्रॉमिस फ़ंक्शन (जैसे, फिर और पाइप) ट्रिगर हो जाएंगे.

इस्तेमाल के उदाहरण

यहां इस्तेमाल के कुछ ऐसे अच्छे उदाहरण दिए गए हैं जिनमें "स्थगित किया गया" बहुत मददगार हो सकता है:

डेटा ऐक्सेस: डेटा ऐक्सेस एपीआई को $.स्थगित करना अक्सर सही डिज़ाइन होता है. रिमोट डेटा के लिए ऐसा करना साफ़-साफ़ ज़ाहिर है, क्योंकि सिंक्रोनस रिमोट कॉल से उपयोगकर्ता अनुभव पूरी तरह से खराब हो सकता है. हालांकि, लोकल डेटा के लिए भी ऐसा अक्सर इसलिए होता है, क्योंकि निचले लेवल के एपीआई (उदाहरण के लिए, SQLite और IndexedDB) अपने-आप एसिंक्रोनस होते हैं. DeFord API की $.when और .Pipe एसिंक्रोनस सब-क्वेरी को सिंक करने और चेन करने के लिए बेहद असरदार हैं.

यूज़र इंटरफ़ेस (यूआई) ऐनिमेशन: ट्रांज़िशनEnd इवेंट के साथ एक या ज़्यादा ऐनिमेशन तैयार करना काफ़ी मुश्किल हो सकता है, खास तौर पर तब, जब ऐनिमेशन में CSS3 ऐनिमेशन और JavaScript का मिला-जुला रूप हो (जैसा कि अक्सर होता है). ऐनिमेशन फ़ंक्शन को 'स्थगित' के रूप में रैप करने से कोड की जटिलता कम हो सकती है और लचीलापन बेहतर हो सकता है. cssAnimation(className) जैसा एक आसान रैपर फ़ंक्शन भी अच्छा काम कर सकता है, जो Promise ऑब्जेक्ट को लौटाएगा.

यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट डिसप्ले: यह थोड़ा बेहतर है, लेकिन बेहतर एचटीएमएल कॉम्पोनेंट फ़्रेमवर्क में भी 'स्थगित' का इस्तेमाल किया जाना चाहिए. बहुत ज़्यादा जानकारी दिए बिना (यह दूसरे पोस्ट का विषय होगा), जब किसी ऐप्लिकेशन को यूज़र इंटरफ़ेस के अलग-अलग हिस्सों को दिखाने की ज़रूरत होती है, तब उन कॉम्पोनेंट की लाइफ़साइकल को 'स्थगित' में एन्क्रिप्ट करने से, समय को ज़्यादा कंट्रोल किया जा सकता है.

कोई भी ब्राउज़र एसिंक्रोनस एपीआई: इसे सामान्य बनाने के लिए, ब्राउज़र एपीआई कॉल को 'स्थगित' के तौर पर रैप करना अच्छा रहता है. इसमें कोड की शब्दश: 4 से 5 लाइनें होती हैं, लेकिन यह किसी भी ऐप्लिकेशन कोड को काफ़ी आसान बना देगा. जैसा कि ऊपर दिए गए getData/getLocation pseudo code में दिखाया गया है, इससे ऐप्लिकेशन कोड को सभी तरह के एपीआई (ब्राउज़र, ऐप्लिकेशन की खास बातों, और कंपाउंड) में एक एसिंक्रोनस मॉडल बनाने की अनुमति मिलती है.

कैश मेमोरी में सेव करना: यह एक तरह का फ़ायदा है, लेकिन कुछ मामलों में यह बहुत काम का हो सकता है. क्योंकि Promise API (उदाहरण के लिए, .done(...) और .fail(...)) को एसिंक्रोनस कॉल किए जाने से पहले या बाद में कॉल किया जा सकता है, स्थगित ऑब्जेक्ट को एसिंक्रोनस कॉल के लिए कैशिंग हैंडल के रूप में इस्तेमाल किया जा सकता है. उदाहरण के लिए, CashManager सिर्फ़ दिए गए अनुरोधों के लिए स्थगित किए गए डेटा को ट्रैक कर सकता है और अगर मैच करने वाले 'स्थगित' को अमान्य नहीं माना गया हो, तो उसका प्रॉमिसेस दिखा सकता है. सबसे अच्छी बात यह है कि कॉल करने वाले को कॉल का जवाब मिलने या न होने की जानकारी देने की ज़रूरत नहीं होती. इसके कॉलबैक फ़ंक्शन को ठीक इसी तरह कॉल किया जाएगा.

नतीजा

हालांकि, $.De लिविंग की प्रोसेस आसान है, लेकिन इस पर अच्छा हैंडल मिलने में समय लग सकता है. हालांकि, ब्राउज़र एनवायरमेंट की वजह से, JavaScript में एसिंक्रोनस प्रोग्रामिंग में महारत हासिल करना किसी भी गंभीर HTML5 ऐप्लिकेशन डेवलपर के लिए बेहद ज़रूरी है. साथ ही, प्रॉमिस पैटर्न (और jQuery लागू करना) बेहतरीन टूल हैं, ताकि एसिंक्रोनस प्रोग्रामिंग को भरोसेमंद और बेहतर बनाया जा सके.