प्रॉमिस, देर से और एसिंक्रोनस तरीके से होने वाले कैलकुलेशन को आसान बनाते हैं. प्रॉमिस ऐसी कार्रवाई को दिखाता है जो अभी तक पूरी नहीं हुई है.
डेवलपर, वेब डेवलपमेंट के इतिहास में एक अहम पलों के लिए खुद को तैयार करें.
[Drumroll begins]
JavaScript में Promises आ गए हैं!
[आतिशबाज़ी होती है, ऊपर से चमकदार कागज़ की बारिश होती है, और भीड़ खुश हो जाती है]
इस समय, आप इनमें से किसी एक कैटगरी में आते हैं:
- आपके आस-पास के लोग जय-जयकार कर रहे हैं, लेकिन आपको नहीं पता कि इतनी खुशी क्यों है. शायद आपको यह भी नहीं पता कि "वादा" क्या होता है. आपने शायद इस बात को अनदेखा कर दिया हो, लेकिन ग्लिटर वाले कागज़ का वज़न आपके कंधे पर पड़ रहा है. अगर ऐसा है, तो इसकी चिंता न करें. मुझे इस बात का पता लगाने में काफ़ी समय लगा कि मुझे इस तरह की चीज़ों की परवाह क्यों करनी चाहिए. शायद आपको शुरुआत से शुरू करना हो.
- आपने हवा में मुक्का मारा! क्या समय सही है? आपने पहले इन 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
});
बुनियादी तौर पर, प्रॉमिस कुछ हद तक इवेंट लिसनर जैसे होते हैं. हालांकि, इनमें ये फ़र्क़ होते हैं:
- कोई वादा सिर्फ़ एक बार पूरा या फ़ेल हो सकता है. यह दो बार सफल या असफल नहीं हो सकता. साथ ही, यह सफल से असफल या असफल से सफल नहीं हो सकता.
- अगर कोई प्रॉमिस पूरा हो गया है या पूरा नहीं हो सका है और आपने बाद में सफलता/असफलता के लिए कॉलबैक जोड़ा है, तो सही कॉलबैक को कॉल किया जाएगा. भले ही, इवेंट पहले हो चुका हो.
यह असाइनमेंट के पूरा होने या न होने के बारे में जानकारी पाने के लिए काफ़ी मददगार है. ऐसा इसलिए, क्योंकि आपको किसी असाइनमेंट के उपलब्ध होने के सटीक समय के बारे में कम और उसके नतीजे के बारे में ज़्यादा जानकारी चाहिए.
प्रोमिस शब्दावली
डॉमिनिक डेनिकोला प्रूफ़ ने इस लेख का पहला ड्राफ़्ट पढ़ा और शब्दावली के लिए मुझे "F" ग्रेड दिया. उन्होंने मुझे निलंबित कर दिया, मुझे राज्य और किस्मत को 100 बार कॉपी करने के लिए कहा, और मेरे माता-पिता को चिंता में डालने वाला पत्र लिखा. इसके बावजूद, मुझे अब भी बहुत सारी शब्दावली मिलती-जुलती हैं, लेकिन ये रही बुनियादी बातें:
वादा इनमें से कोई हो सकता है:
- fullfill - प्रॉमिस से जुड़ी कार्रवाई पूरी हो गई
- अस्वीकार किया गया - वादे से जुड़ी कार्रवाई पूरी नहीं हो सकी
- मंज़ूरी बाकी है - अभी तक स्वीकार या अस्वीकार नहीं किया गया है
- settled - पूरा हो गया है या अस्वीकार कर दिया गया है
स्पेसिफ़िकेशन में, thenable शब्द का इस्तेमाल भी किया जाता है. यह शब्द, किसी ऐसे ऑब्जेक्ट के बारे में बताता है जो वादा करने जैसा होता है. इसमें then
तरीका होता है. यह शब्द मुझे इंग्लैंड के पूर्व फ़ुटबॉल मैनेजर
टेरी वेनेबल्स की याद दिलाता है
इसलिए, मैं इसका कम से कम इस्तेमाल करूंगा.
JavaScript में प्रॉमिस की सुविधा!
वादों को लाइब्रेरी के रूप में लंबे समय से इस्तेमाल किया जा रहा है, जैसे कि:
ऊपर दिए गए और JavaScript के वादों में एक सामान्य और स्टैंडर्ड बिहेवियर शेयर किया जाता है जिसे प्रॉमिस/A+ कहते हैं. अगर आप एक jQuery उपयोगकर्ता हैं, तो उनमें कुछ ऐसा मिलता-जुलता है डिफ़र्ड. हालांकि, डिफ़र्ड, Promise/A+ के मुताबिक नहीं होते. इस वजह से, ये थोड़े अलग और कम काम के होते हैं. इसलिए, इनसे सावधान रहें. jQuery में भी Promise टाइप होता है, लेकिन यह डिफ़र्ड का सिर्फ़ एक सबसेट है और इसमें वही समस्याएं होती हैं.
हालांकि, वादा लागू करने के तरीके एक जैसे होते हैं, लेकिन उनके एपीआई अलग-अलग होते हैं. JavaScript के प्रॉमिस, एपीआई में और response.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. कॉलबैक में कुछ करें, शायद असाइन किए गए टास्क को सिंक किए बिना. इसके बाद, अगर सब कुछ ठीक से काम करता है, तो कॉल रिज़ॉल्व करें. अगर ऐसा नहीं है, तो कॉल अस्वीकार करें.
आम तौर पर, Error ऑब्जेक्ट के साथ अस्वीकार करना ज़रूरी नहीं है, लेकिन यह throw
के जैसे ही है. गड़बड़ी वाले ऑब्जेक्ट का फ़ायदा यह है कि वे स्टैक ट्रेस कैप्चर करते हैं. इससे, डीबग करने वाले टूल ज़्यादा मददगार बन जाते हैं.
इस वादे का इस्तेमाल करने का तरीका यहां बताया गया है:
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 का इस्तेमाल करने के लिए, अपने get फ़ंक्शन में बदलाव किया
responseType
, लेकिन वादों के हिसाब से भी हम इसे हल कर सकते हैं:
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]
फ़ेच नहीं हो पाता है (उदाहरण के लिए, एचटीटीपी 500 या उपयोगकर्ता ऑफ़लाइन है), तो यह आगे मिलने वाले सभी सक्सेस कॉलबैक को छोड़ देगा, जिसमें getJSON()
में से एक कॉलबैक शामिल है.यह कॉलबैक, JSON के तौर पर रिस्पॉन्स को पार्स करने की कोशिश करता है. साथ ही, पेज पर चैप्टर1.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)
भी है, जो ऐसा प्रॉमिस बनाता है जो आपकी दी गई वैल्यू (या तय नहीं किया गया) के साथ अस्वीकार करता है.
हम
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" वैरिएबल की ज़रूरत नहीं है. कलेक्शन में मौजूद हर आइटम के लिए, कम करने के कॉलबैक का इस्तेमाल किया जाता है.
"सीक्वेंस" पहली बार में Promise.resolve()
है, लेकिन बाकी के कॉल "सीक्वेंस" में वह वैल्यू है जो हमने पिछले कॉल से दिखाई है. 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 फ़ंक्शन, दोनों के लिए ज़्यादातर जगहों पर सहायता मिलती है. इसकी जानकारी एमडीएन के प्रॉमाइज़ और एक साथ काम करने वाले फ़ंक्शन के रेफ़रंस में देखी जा सकती है.
ऐन वैन केस्टरिन, डॉमिनिक डेनिकोला, टॉम ऐशवर्थ, रेमी शार्प, ऐडी ओसमानी, आर्थर इवांस, और युताका हिरानो का बहुत-बहुत धन्यवाद, जिन्होंने इस लेख की प्रूफ़रीडिंग की और इसमें सुधार/सुझाव दिए.
साथ ही, लेख के अलग-अलग हिस्सों को अपडेट करने के लिए, मैथियास बीन्स का धन्यवाद.