प्रॉमिस, देर से और अलग-अलग समय पर होने वाले कैलकुलेशन को आसान बनाते हैं. प्रॉमिस, किसी ऐसी कार्रवाई के बारे में बताता है जो अभी तक पूरी नहीं हुई है.
डेवलपर, वेब डेवलपमेंट के इतिहास में एक अहम पलों के लिए खुद को तैयार करें.
[Drumroll begins]
JavaScript में प्रॉमिस आ गए हैं!
[आतिशबाज़ी होती है, ऊपर से चमकदार कागज़ की बारिश होती है, और भीड़ खुश हो जाती है]
इस समय, आप इनमें से किसी एक कैटगरी में आते हैं:
- आपके आस-पास के लोग जय-जयकार कर रहे हैं, लेकिन आपको नहीं पता कि इतनी ज़्यादा खुशी क्यों है. शायद आपको यह भी पता न हो कि "वादा" क्या होता है. आपने शायद इस बात को अनदेखा कर दिया हो, लेकिन ग्लिटर वाले कागज़ का वज़न आपके कंधे पर पड़ रहा है. अगर ऐसा है, तो इसकी चिंता न करें. मुझे इस बात का पता लगाने में काफ़ी समय लगा कि मुझे इस तरह की चीज़ों की परवाह क्यों करनी चाहिए. शायद आपको शुरुआत से शुरू करना हो.
- आपने हवा में मुक्का मारा! क्या समय सही है? आपने पहले भी Promise का इस्तेमाल किया है, लेकिन आपको यह परेशानी होती है कि सभी इंप्लीमेंटेशन में एपीआई थोड़ा अलग होता है. आधिकारिक JavaScript वर्शन के लिए एपीआई क्या है? शायद आपको टर्मिनोलॉजी से शुरुआत करनी हो.
- आपको इस बारे में पहले से पता था और आप उन लोगों को देखकर हंस रहे हैं जो इस बदलाव को नई बात मानकर उत्साहित हैं. अपनी बेहतरीन उपलब्धि पर कुछ समय के लिए गर्व करें. इसके बाद, सीधे एपीआई रेफ़रंस पर जाएं.
ब्राउज़र के साथ काम करना और पॉलीफ़िल
जिन ब्राउज़र में पूरी तरह से प्रॉमिस लागू नहीं किए गए हैं उन्हें स्पेसिफ़िकेशन के मुताबिक बनाने के लिए या अन्य ब्राउज़र और Node.js में प्रॉमिस जोड़ने के लिए, पॉलीफ़िल (2k ज़िप किया गया) देखें.
इस बारे में इतनी चर्चा क्यों हो रही है?
JavaScript सिंगल थ्रेड वाला है. इसका मतलब है कि स्क्रिप्ट के दो हिस्से एक साथ नहीं चल सकते. उन्हें एक के बाद एक चलाना होगा. ब्राउज़र में, JavaScript कई अन्य चीज़ों के साथ एक थ्रेड शेयर करता है. यह थ्रेड, ब्राउज़र के हिसाब से अलग-अलग होती है. हालांकि, आम तौर पर JavaScript, पेज को पेंट करने, स्टाइल को अपडेट करने, और उपयोगकर्ता की कार्रवाइयों को मैनेज करने वाली प्रोसेस के साथ एक ही कतार में होता है. जैसे, टेक्स्ट को हाइलाइट करना और फ़ॉर्म कंट्रोल के साथ इंटरैक्ट करना. इनमें से किसी एक गतिविधि में गतिविधि करने पर, बाकी गतिविधियों में देरी होती है.
इंसान के तौर पर, आपके पास कई काम करने की क्षमता होती है. एक से ज़्यादा उंगलियों से टाइप किया जा सकता है. साथ ही, एक ही समय पर गाड़ी चलाई जा सकती है और बातचीत की जा सकती है. हमें सिर्फ़ छींकने की वजह से होने वाली रुकावट से निपटना पड़ता है. इस दौरान, छींकने की अवधि के लिए सभी मौजूदा गतिविधियों को निलंबित कर दिया जाना चाहिए. यह काफ़ी परेशान करने वाला होता है, खास तौर पर तब, जब गाड़ी चलाते समय बातचीत की जा रही हो. आपको ऐसा कोड नहीं लिखना है जो स्नीज़ी हो.
शायद आपने इस समस्या से बचने के लिए, इवेंट और कॉलबैक का इस्तेमाल किया हो. इवेंट यहां दिए गए हैं:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
यह बिलकुल भी स्मार्ट नहीं है. हम इमेज पाते हैं और कुछ लिसनर जोड़ते हैं. इसके बाद, जब तक उनमें से किसी एक लिसनर को कॉल नहीं किया जाता, तब तक JavaScript को एक्सीक्यूट करना बंद किया जा सकता है.
माफ़ करें, ऊपर दिए गए उदाहरण में, ऐसा हो सकता है कि इवेंट, इन्हें सुनने से पहले ही हो गए हों. इसलिए, हमें इमेज की "complete" प्रॉपर्टी का इस्तेमाल करके, इस समस्या को हल करना होगा:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
इससे उन इमेज का पता नहीं चलता जिनमें गड़बड़ी होने से पहले, हम उन्हें देख नहीं पाते. माफ़ करें, डीओएम में ऐसा करने का कोई तरीका नहीं है. साथ ही, यह एक इमेज लोड कर रहा है. अगर हमें यह जानना है कि इमेज का कोई सेट कब लोड हुआ, तो चीज़ें और भी मुश्किल हो जाती हैं.
इवेंट हमेशा सबसे अच्छा तरीका नहीं होते
इवेंट उन चीज़ों के लिए बेहतरीन होते हैं जो एक ही ऑब्जेक्ट—keyup
, touchstart
वगैरह पर कई बार हो सकते हैं. इन इवेंट के लिए, आपको यह जानने की ज़रूरत नहीं होती कि ऑब्जेक्ट पर, ऑडियंस को जोड़ने से पहले क्या हुआ था. हालांकि, जब एक साथ कई टास्क पूरे होने/पूरे न होने की बात आती है, तो आपको कुछ ऐसा चाहिए:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
वादे भी यही काम करते हैं, लेकिन बेहतर नाम के साथ. अगर एचटीएमएल इमेज एलिमेंट में ऐसा "तैयार" तरीका होता है जो एक प्रॉमिस दिखाता है, तो हम ऐसा कर सकते हैं:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
बुनियादी तौर पर, प्रॉमिस कुछ हद तक इवेंट लिसनर जैसे होते हैं. हालांकि, इनमें ये फ़र्क़ होते हैं:
- किसी वादे को पूरा या पूरा न करने की स्थिति सिर्फ़ एक बार हो सकती है. यह दो बार सफल या असफल नहीं हो सकता. साथ ही, यह सफल से असफल या असफल से सफल नहीं हो सकता.
- अगर कोई प्रॉमिस पूरा हो गया है या पूरा नहीं हो सका है और आपने बाद में सफलता/असफलता के लिए कॉलबैक जोड़ा है, तो सही कॉलबैक को कॉल किया जाएगा. भले ही, इवेंट पहले हो चुका हो.
यह असाइनमेंट के पूरा होने या न होने के बारे में जानकारी पाने के लिए काफ़ी मददगार है. ऐसा इसलिए, क्योंकि आपको किसी चीज़ के उपलब्ध होने के सटीक समय के बारे में कम और नतीजे के बारे में ज़्यादा जानकारी चाहिए.
प्रॉमिस की शब्दावली
डोमेनिक डेनिकोला ने इस लेख के पहले ड्राफ़्ट की प्रूफ़रीडिंग की और मुझे शब्दावली के लिए "एफ़" ग्रेड दिया. उन्होंने मुझे निलंबित कर दिया, मुझे स्टेटस ऐंड फ़ेट को 100 बार कॉपी करने के लिए कहा, और मेरे माता-पिता को चिंता में डालने वाला एक पत्र लिखा. इसके बावजूद, मुझे अब भी कई शब्दों को समझने में मुश्किल होती है. हालांकि, यहां कुछ बुनियादी शब्दों के बारे में बताया गया है:
वादा इनमें से कोई हो सकता है:
- पूरा हो गया - वादे से जुड़ी कार्रवाई पूरी हो गई
- अस्वीकार किया गया - वादे से जुड़ी कार्रवाई पूरी नहीं हो सकी
- मंज़ूरी बाकी है - अभी तक स्वीकार या अस्वीकार नहीं किया गया है
- settled - पूरा हो गया है या अस्वीकार कर दिया गया है
स्पेसिफ़िकेशन में, thenable शब्द का इस्तेमाल भी किया जाता है. यह शब्द, किसी ऐसे ऑब्जेक्ट के बारे में बताता है जो वादा करने जैसा होता है. इसमें then
तरीका होता है. इस शब्द से मुझे इंग्लैंड के फ़ुटबॉल मैनेजर टैरी वेनबल्स की याद आती है. इसलिए, मैं इसका इस्तेमाल कम से कम करूंगा.
JavaScript में प्रॉमिस की सुविधा!
लाइब्रेरी के तौर पर, Promises का इस्तेमाल पहले से किया जा रहा है. जैसे:
ऊपर दिए गए और JavaScript के प्रॉमिस एक जैसे और स्टैंडर्ड तरीके से काम करते हैं. इन्हें Promises/A+ कहा जाता है. अगर आप jQuery के उपयोगकर्ता हैं, तो आपके पास भी कुछ ऐसा ही है, जिसे Deferreds कहा जाता है. हालांकि, डिफ़र्ड, Promise/A+ के मुताबिक नहीं होते. इस वजह से, ये थोड़े अलग और कम काम के होते हैं. इसलिए, इनसे सावधान रहें. jQuery में भी Promise टाइप होता है, लेकिन यह डिफ़र्ड का सिर्फ़ एक सबसेट है और इसमें वही समस्याएं होती हैं.
हालांकि, वादा लागू करने के तरीके एक जैसे होते हैं, लेकिन उनके एपीआई अलग-अलग होते हैं. JavaScript के प्रॉमिस, RSVP.js के एपीआई से मिलते-जुलते हैं. वादा बनाने का तरीका यहां बताया गया है:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
Promise कंस्ट्रक्टर एक आर्ग्युमेंट लेता है, जो दो पैरामीटर वाला कॉलबैक होता है, resolve और reject. कॉलबैक में कुछ करें, शायद असाइन किए गए टास्क को सिंक किए बिना. इसके बाद, अगर सब कुछ ठीक से काम करता है, तो कॉल रिज़ॉल्व करें. अगर ऐसा नहीं है, तो कॉल अस्वीकार करें.
आम JavaScript में throw
की तरह, Error ऑब्जेक्ट के साथ अस्वीकार करना आम बात है. हालांकि, ऐसा करना ज़रूरी नहीं है. गड़बड़ी वाले ऑब्जेक्ट का फ़ायदा यह है कि वे स्टैक ट्रेस कैप्चर करते हैं. इससे, डीबगिंग टूल ज़्यादा मददगार बनते हैं.
इस वादे का इस्तेमाल करने का तरीका यहां बताया गया है:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
में दो आर्ग्युमेंट होते हैं. पहला, सफलता के मामले में कॉलबैक और दूसरा, गड़बड़ी के मामले में कॉलबैक. दोनों ज़रूरी नहीं हैं. इसलिए, सिर्फ़ सफल या गड़बड़ी वाले मामले के लिए कॉलबैक जोड़ा जा सकता है.
JavaScript के प्रॉमिस, DOM में "फ़्यूचर" के तौर पर शुरू हुए थे. बाद में, इनका नाम बदलकर "प्रॉमिस" कर दिया गया. आखिर में, इन्हें JavaScript में शामिल कर लिया गया. इन्हें डीओएम के बजाय JavaScript में रखना बेहतर है, क्योंकि ये Node.js जैसे ब्राउज़र के बाहर के JS कॉन्टेक्स्ट में उपलब्ध होंगे. हालांकि, यह एक अलग सवाल है कि वे अपने मुख्य एपीआई में इनका इस्तेमाल करते हैं या नहीं.
हालांकि, ये JavaScript की सुविधाएं हैं, लेकिन DOM इनका इस्तेमाल करने से नहीं डरता. असल में, सभी नए DOM एपीआई, जिनमें असाइन किए गए काम के पूरा होने या न होने के तरीके असाइन किए गए हैं वे प्रॉमिस का इस्तेमाल करेंगे. यह पहले से ही कोटा मैनेजमेंट, फ़ॉन्ट लोड इवेंट, ServiceWorker, Web MIDI, स्ट्रीम वगैरह के साथ हो रहा है.
अन्य लाइब्रेरी के साथ काम करना
JavaScript promises API, then()
तरीके का इस्तेमाल करने वाली किसी भी चीज़ को प्रॉमिस की तरह (या प्रॉमिस के हिसाब से सांस में thenable
) मानेगा. इसलिए, अगर आपने ऐसी लाइब्रेरी का इस्तेमाल किया है जो Q प्रॉमिस दिखाती है, तो कोई बात नहीं. यह नई JavaScript प्रॉमिस के साथ अच्छी तरह काम करेगी.
हालांकि, जैसा कि मैंने बताया, jQuery के Deferreds थोड़े … काम के नहीं हैं. शुक्र है कि उन्हें स्टैंडर्ड प्रॉमिस में बदला जा सकता है. ऐसा जल्द से जल्द करना चाहिए:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
यहां, jQuery का $.ajax
एक Deferred दिखाता है. इसमें then()
तरीका है, इसलिए Promise.resolve()
इसे JavaScript प्रॉमिस में बदल सकता है. हालांकि, कभी-कभी डिफ़र्ड फ़ंक्शन अपने कॉलबैक में कई आर्ग्युमेंट पास करते हैं. उदाहरण के लिए:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
वहीं, JS के प्रॉमिस में पहले प्रॉमिस को छोड़कर बाकी सभी को अनदेखा कर दिया जाता है:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
आम तौर पर, आपको यही चाहिए या कम से कम आपको वही ऐक्सेस मिलता है जो आपको चाहिए. यह भी ध्यान रखें कि jQuery, अस्वीकार किए गए आइटम में गड़बड़ी वाले ऑब्जेक्ट को पास करने के नियम का पालन नहीं करता.
एसिंक्रोनस कोड को आसान बनाना
ठीक है, चलिए कुछ कोड लिखते हैं. मान लें कि हमें:
- लोड होने की जानकारी देने के लिए स्पिनर दिखाना
- किसी कहानी के लिए कुछ JSON फ़ेच करें, जिससे हमें हर चैप्टर का टाइटल और यूआरएल मिलता है
- पेज में टाइटल जोड़ना
- हर चैप्टर को फ़ेच करना
- पेज में स्टोरी जोड़ना
- स्पिनर को रोकना
… साथ ही, अगर कोई गड़बड़ी होती है, तो उपयोगकर्ता को इसकी जानकारी भी दें. हम उस समय भी स्पिनर को रोकना चाहेंगे, नहीं तो वह स्पिन करता रहेगा, उसे चक्कर आएगा, और वह किसी दूसरे यूज़र इंटरफ़ेस (यूआई) में क्रैश हो जाएगा.
बेशक, किसी स्टोरी को डिलीवर करने के लिए JavaScript का इस्तेमाल नहीं किया जाएगा, क्योंकि एचटीएमएल के तौर पर दिखाना ज़्यादा तेज़ होता है. हालांकि, एपीआई का इस्तेमाल करते समय यह पैटर्न काफ़ी आम है: डेटा को कई बार फ़ेच किया जाता है और फिर सभी काम पूरा होने के बाद कुछ किया जाता है.
सबसे पहले, नेटवर्क से डेटा फ़ेच करने के बारे में जानें:
XMLHttpRequest को प्रॉमिस में बदलना
अगर पुराने एपीआई को नए वर्शन के साथ काम करने लायक बनाया जा सकता है, तो उन्हें अपडेट किया जाएगा. XMLHttpRequest
एक मुख्य उम्मीदवार है, लेकिन इस बीच, एक GET अनुरोध करने के लिए एक आसान फ़ंक्शन लिखें:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
अब इसका इस्तेमाल करते हैं:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
अब हम XMLHttpRequest
को मैन्युअल तरीके से टाइप किए बिना एचटीटीपी अनुरोध कर सकते हैं. यह बहुत अच्छा है, क्योंकि XMLHttpRequest
के कैमल-केसिंग को देखना मुझे पसंद नहीं है.
चेन
then()
के बाद भी, वैल्यू बदलने या एक के बाद एक अतिरिक्त असाइनक कार्रवाई करने के लिए, then
को एक साथ जोड़ा जा सकता है.
वैल्यू बदलना
नई वैल्यू दिखाकर, वैल्यू में आसानी से बदलाव किया जा सकता है:
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
})
उदाहरण के लिए, आइए वापस उस उदाहरण पर जाएं:
get('story.json').then(function(response) {
console.log("Success!", response);
})
जवाब JSON फ़ॉर्मैट में है, लेकिन फ़िलहाल हमें यह सादे टेक्स्ट के तौर पर मिल रहा है. हम JSON responseType
का इस्तेमाल करने के लिए, अपने get फ़ंक्शन में बदलाव कर सकते हैं. हालांकि, हम इसे प्रॉमिस के ज़रिए भी हल कर सकते हैं:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
JSON.parse()
एक आर्ग्युमेंट लेता है और बदली गई वैल्यू दिखाता है. इसलिए, हम इसका शॉर्टकट बना सकते हैं:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
असल में, हम getJSON()
फ़ंक्शन को आसानी से बना सकते हैं:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON()
अब भी एक प्रॉमिस दिखाता है, जो यूआरएल को फ़ेच करता है और फिर रिस्पॉन्स को JSON के तौर पर पार्स करता है.
एसिंक्रोनस कार्रवाइयों को सूची में जोड़ना
then
को चेन में जोड़कर, क्रम से असाइन किए गए ऐक्शन चलाए जा सकते हैं.
then()
कॉलबैक से कोई आइटम वापस करने पर, यह थोड़ा जादुई लगता है.
अगर कोई वैल्यू दी जाती है, तो अगले then()
को उस वैल्यू के साथ कॉल किया जाता है. हालांकि, अगर आपने कोई ऐसा वैल्यू लौटाई है जो वादा करने जैसी है, तो अगला then()
उस पर इंतज़ार करता है और उसे सिर्फ़ तब कॉल किया जाता है, जब वह वादा पूरा हो जाता है (सफल होता है/फ़ेल होता है). उदाहरण के लिए:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
यहां हम story.json
के लिए एसिंक्रोनस रिक्वेस्ट करते हैं. इससे हमें अनुरोध करने के लिए यूआरएल का एक सेट मिलता है. इसके बाद, हम उनमें से पहले यूआरएल का अनुरोध करते हैं. यहीं से वादे, आसान कॉलबैक पैटर्न से अलग दिखने लगते हैं.
चैप्टर देखने के लिए, शॉर्टकट भी बनाया जा सकता है:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
जब तक getChapter
को कॉल नहीं किया जाता, तब तक हम story.json
को डाउनलोड नहीं करते. हालांकि, अगली बार getChapter
को कॉल करने पर, हम स्टोरी प्रॉमिस का फिर से इस्तेमाल करते हैं. इसलिए, story.json
को सिर्फ़ एक बार फ़ेच किया जाता है. वाह, वादे!
गड़बड़ी ठीक करना
जैसा कि हमने पहले देखा था, then()
में दो आर्ग्युमेंट होते हैं. एक सफलता के लिए, एक असफलता के लिए (या वादे के हिसाब से, पूरा करने और अस्वीकार करने के लिए):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
catch()
का इस्तेमाल इनके लिए भी किया जा सकता है:
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
catch()
में कुछ खास नहीं है. यह then(undefined, func)
के लिए सिर्फ़ एक शुगर है, लेकिन इसे पढ़ना आसान है. ध्यान दें कि ऊपर दिए गए दो कोड के उदाहरण एक जैसे नहीं हैं. दूसरा उदाहरण, इस तरह काम करता है:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
यह फ़र्क़ बहुत कम है, लेकिन बहुत काम का है. वादा अस्वीकार होने पर, अगले then()
पर सीधे तौर पर पहुंचा जाता है. इसके लिए, अस्वीकार करने के कॉलबैक का इस्तेमाल किया जाता है. इसके अलावा, catch()
का इस्तेमाल भी किया जा सकता है, क्योंकि यह then()
के बराबर है. then(func1, func2)
के साथ, func1
या func2
को कभी भी एक साथ नहीं, बल्कि कभी एक तो कभी दूसरा फ़ंक्शन कॉल किया जाएगा. हालांकि, then(func1).catch(func2)
के साथ, अगर func1
अस्वीकार करता है, तो दोनों को कॉल किया जाएगा, क्योंकि वे चेन में अलग-अलग चरण हैं. ये काम करें:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
ऊपर दिया गया फ़्लो, सामान्य JavaScript try/catch से काफ़ी मिलता-जुलता है. "try" में होने वाली गड़बड़ियां, तुरंत catch()
ब्लॉक में चली जाती हैं. यहां ऊपर दी गई जानकारी को फ़्लोचार्ट के तौर पर दिखाया गया है, क्योंकि मुझे फ़्लोचार्ट पसंद हैं:
पूरे किए गए वादे के लिए नीली लाइनों का पालन करें या अस्वीकार किए गए वादे के लिए लाल लाइनों का पालन करें.
JavaScript अपवाद और प्रॉमिस
किसी प्रॉमिस को साफ़ तौर पर अस्वीकार करने पर, उसे अस्वीकार कर दिया जाता है. हालांकि, अगर कन्स्ट्रक्टर कॉलबैक में कोई गड़बड़ी होती है, तो भी उसे अस्वीकार कर दिया जाता है:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
इसका मतलब है कि प्रोमिस से जुड़ा सारा काम, प्रोमिस के कॉन्स्ट्रक्टर कॉलबैक में करना फ़ायदेमंद होता है. इससे गड़बड़ियां अपने-आप पकड़ी जाती हैं और अस्वीकार कर दी जाती हैं.
then()
कॉलबैक में आने वाली गड़बड़ियों पर भी यही बात लागू होती है.
get('/').then(JSON.parse).then(function() {
// This never happens, '/' is an HTML page, not JSON
// so JSON.parse throws
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
गड़बड़ी ठीक करने का तरीका
अपनी स्टोरी और चैप्टर में, हम उपयोगकर्ता को गड़बड़ी दिखाने के लिए catch का इस्तेमाल कर सकते हैं:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage("Failed to show chapter");
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
अगर story.chapterUrls[0]
फ़ेच नहीं हो पाता है (उदाहरण के लिए, http 500 या उपयोगकर्ता ऑफ़लाइन है), तो यह रिस्पॉन्स मिलने के बाद होने वाले सभी कॉलबैक को स्किप कर देगा.इनमें getJSON()
में मौजूद वह कॉलबैक भी शामिल है जो रिस्पॉन्स को JSON के तौर पर पार्स करने की कोशिश करता है. साथ ही, यह उस कॉलबैक को भी स्किप कर देगा जो पेज में chapter1.html जोड़ता है. इसके बजाय, यह catch callback पर चला जाता है. इस वजह से, अगर कोई भी पिछली कार्रवाई पूरी नहीं होती है, तो पेज पर "चैप्टर नहीं दिखाया जा सका" मैसेज जोड़ दिया जाएगा.
JavaScript के try/catch की तरह, गड़बड़ी को पकड़ लिया जाता है और इसके बाद का कोड काम करना जारी रखता है. इसलिए, स्पिनर हमेशा छिपा रहता है, जो कि हमारी ज़रूरत है. ऊपर दिया गया कोड, इन फ़ंक्शन का नॉन-ब्लॉकिंग असाइनिक प्रोग्राम बन जाता है:
try {
var story = getJSONSync('story.json');
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch (e) {
addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
हो सकता है कि आप catch()
को सिर्फ़ लॉग करने के मकसद से इस्तेमाल करना चाहें, न कि गड़बड़ी को ठीक करने के लिए. ऐसा करने के लिए, गड़बड़ी को फिर से फेंकें. हम getJSON()
तरीके से ऐसा कर सकते हैं:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
इसलिए, हमने एक चैप्टर फ़ेच कर लिया है, लेकिन हमें सभी चैप्टर चाहिए. आइए, ऐसा करते हैं.
पैरलल प्रोसेस और क्रम से प्रोसेस करना: दोनों का ज़्यादा से ज़्यादा फ़ायदा पाना
असाइनमेंट को अलग-अलग क्रम में पूरा करना आसान नहीं है. अगर आपको कोड में बदलाव करने में समस्या आ रही है, तो कोड को सिंक्रोनस तौर पर लिखने की कोशिश करें. इस मामले में:
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
यह काम करेगा! हालांकि, डाउनलोड होने के दौरान, यह ब्राउज़र को सिंक और लॉक कर देता है. इस प्रोसेस को एसिंक्रोनस बनाने के लिए, हम then()
का इस्तेमाल करते हैं, ताकि एक के बाद एक चीज़ें हो सकें.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
लेकिन चैप्टर के यूआरएल को लूप में कैसे डालें और उन्हें क्रम से फ़ेच कैसे करें? यह तरीका इनमें काम नहीं करता:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach
में, अलग-अलग क्रम में डाउनलोड किए जाने की सुविधा नहीं है. इसलिए, हमारे चैप्टर उसी क्रम में दिखेंगे जिस क्रम में वे डाउनलोड किए गए हैं. मूल रूप से, Pulp Fiction को इसी तरह लिखा गया था. यह पुलप फ़िक्शन नहीं है, इसलिए इसे ठीक करते हैं.
क्रम बनाना
हमें अपने chapterUrls
कलेक्शन को, प्रॉमिस के क्रम में बदलना है. हम then()
का इस्तेमाल करके ऐसा कर सकते हैं:
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
हमें पहली बार Promise.resolve()
मिला है. यह एक ऐसा प्रोमिस है जो किसी भी वैल्यू को रिज़ॉल्व करता है. अगर इसे Promise
का कोई उदाहरण दिया जाता है, तो यह उसे वापस कर देगा (ध्यान दें: यह स्पेसिफ़िकेशन में किया गया बदलाव है, जिसे कुछ लागू करने के तरीके अब तक फ़ॉलो नहीं करते). अगर इसे किसी ऐसे आइटम के तौर पर पास किया जाता है जिसमें then()
तरीका है, तो यह एक असली Promise
बनाता है, जो उसी तरह से पूरा/अस्वीकार होता है. अगर आपने कोई दूसरी वैल्यू दी है, जैसे कि Promise.resolve('Hello')
, यह एक ऐसा प्रोमिस बनाता है जो उस वैल्यू के साथ पूरा होता है. अगर इसे बिना किसी वैल्यू के कॉल किया जाता है, तो ऊपर बताए गए तरीके के मुताबिक, यह "undefined" के साथ पूरा होता है.
Promise.reject(val)
भी एक फ़ंक्शन है. यह एक ऐसा प्रॉमिस बनाता है जो आपकी दी गई वैल्यू (या undefined) के साथ अस्वीकार हो जाता है.
array.reduce
का इस्तेमाल करके, ऊपर दिए गए कोड को बेहतर बनाया जा सकता है:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
यह पिछले उदाहरण की तरह ही काम कर रहा है, लेकिन इसके लिए अलग से "sequence" वैरिएबल की ज़रूरत नहीं है. हमारे reduce कॉलबैक को ऐरे में मौजूद हर आइटम के लिए कॉल किया जाता है.
"sequence" पहली बार Promise.resolve()
होता है, लेकिन बाकी सभी कॉल के लिए "sequence" वही होता है जो हमने पिछले कॉल से लौटाया था. array.reduce
, किसी ऐरे को एक वैल्यू में बदलने के लिए काफ़ी फ़ायदेमंद है. इस मामले में, यह वैल्यू एक प्रॉमिस है.
आइए, सभी बातों को एक साथ जोड़ते हैं:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
और यहाँ हमारे पास, सिंक किए गए वर्शन का पूरी तरह से असाइन्क वर्शन है. हालांकि, हम इसे और बेहतर बना सकते हैं. फ़िलहाल, हमारा पेज इस तरह डाउनलोड हो रहा है:
ब्राउज़र एक साथ कई चीज़ें डाउनलोड करने में काफ़ी अच्छे होते हैं. इसलिए, एक के बाद एक चैप्टर डाउनलोड करने से, हमें परफ़ॉर्मेंस में कमी आ रही है. हम सभी फ़ाइलों को एक साथ डाउनलोड करना चाहते हैं. इसके बाद, सभी फ़ाइलें आने पर उन्हें प्रोसेस करना चाहते हैं. अच्छी बात यह है कि इसके लिए एक एपीआई उपलब्ध है:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
, एक कलेक्शन में मौजूद प्रॉमिस लेता है और एक ऐसा प्रॉमिस बनाता है जो सभी प्रॉमिस पूरे होने पर पूरा होता है. आपको नतीजों का एक कलेक्शन (जो भी किए गए वादे पूरे हुए) उसी क्रम में मिलता है जिस क्रम में आपने वादे किए थे.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
कनेक्शन के हिसाब से, एक-एक करके लोड करने की तुलना में यह कुछ सेकंड तेज़ हो सकता है. साथ ही, इसमें पहले की तुलना में कम कोड का इस्तेमाल किया गया है. चैप्टर किसी भी क्रम में डाउनलोड किए जा सकते हैं, लेकिन वे स्क्रीन पर सही क्रम में दिखते हैं.
हालांकि, हम अब भी अनुमानित परफ़ॉर्मेंस को बेहतर बना सकते हैं. जब पहला चैप्टर उपलब्ध हो जाएगा, तो हमें उसे पेज पर जोड़ देना चाहिए. इससे उपयोगकर्ता, बाकी चैप्टर उपलब्ध होने से पहले ही पढ़ना शुरू कर सकता है. तीसरा चैप्टर आने पर, हम उसे पेज पर नहीं जोड़ेंगे, क्योंकि हो सकता है कि उपयोगकर्ता को पता न चले कि दूसरा चैप्टर मौजूद नहीं है. जब दूसरा चैप्टर आएगा, तो हम दूसरे और तीसरे चैप्टर वगैरह जोड़ सकते हैं.
इसके लिए, हम एक साथ अपने सभी चैप्टर के लिए JSON फ़ेच करते हैं. इसके बाद, उन्हें दस्तावेज़ में जोड़ने के लिए एक क्रम बनाते हैं:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
और अब, दोनों का सबसे अच्छा फ़ायदा! पूरा कॉन्टेंट डिलीवर करने में उतना ही समय लगता है, लेकिन उपयोगकर्ता को शुरुआती कॉन्टेंट जल्दी मिल जाता है.
इस आसान उदाहरण में, सभी चैप्टर एक ही समय पर दिखते हैं. हालांकि, एक बार में एक चैप्टर दिखाने का फ़ायदा, ज़्यादा और बड़े चैप्टर के साथ ज़्यादा होगा.
Node.js-स्टाइल कॉलबैक या इवेंट की मदद से ऊपर बताई गई प्रोसेस को पूरा करने के लिए, कोड का साइज़ करीब दोगुना हो जाता है. हालांकि, सबसे अहम बात यह है कि इसे समझना उतना आसान नहीं है. हालांकि, प्रोमिस के बारे में यही सब नहीं है. ES6 की अन्य सुविधाओं के साथ इस्तेमाल करने पर, प्रोमिस का इस्तेमाल करना और भी आसान हो जाता है.
बोनस राउंड: ज़्यादा सुविधाएं
मैंने यह लेख पहली बार लिखा था, तब से Promises का इस्तेमाल करने की सुविधा का दायरा बहुत बढ़ गया है. Chrome 55 से, असाइनिक फ़ंक्शन की मदद से, प्रॉमिस पर आधारित कोड को वैसे ही लिखा जा सकता है जैसे कि वह सिंक्रोनस हो. हालांकि, ऐसा करने पर मुख्य थ्रेड ब्लॉक नहीं होता. इस बारे में ज़्यादा जानने के लिए, असाइन किए गए फ़ंक्शन के बारे में लेख पढ़ें. आम तौर पर इस्तेमाल होने वाले ब्राउज़र में, Promises और async फ़ंक्शन, दोनों के लिए ज़्यादातर जगहों पर सहायता उपलब्ध होती है. ज़्यादा जानकारी के लिए, MDN के Promise और async function रेफ़रंस देखें.
ऐन वैन केस्टरिन, डॉमिनिक डेनिकोला, टॉम ऐशवर्थ, रेमी शार्प, ऐडी ओसमानी, आर्थर इवांस, और युताका हिरानो का बहुत-बहुत धन्यवाद, जिन्होंने इस लेख की प्रूफ़रीडिंग की और इसमें सुधार/सुझाव दिए.
साथ ही, लेख के अलग-अलग हिस्सों को अपडेट करने के लिए, मैथियास बीन्स का धन्यवाद.