प्रॉमिस, बाद में होने वाली और एसिंक्रोनस कंप्यूटेशन को आसान बनाते हैं. प्रॉमिस, ऐसे ऑपरेशन को दिखाता है जो अब तक पूरा नहीं हुआ है.
डेवलपर, वेब डेवलपमेंट के इतिहास में एक अहम पल के लिए खुद को तैयार करें.
[ड्रमरोल शुरू होता है]
JavaScript में प्रॉमिस की सुविधा उपलब्ध हो गई है!
[आतिशबाज़ी हो रही है, ऊपर से चमकीले काग़ज़ गिर रहे हैं, और भीड़ झूम रही है]
इस समय, आप इनमें से किसी एक कैटगरी में आते हैं:
- आपके आस-पास लोग जश्न मना रहे हैं, लेकिन आपको नहीं पता कि यह जश्न किस बात का है. शायद आपको यह भी नहीं पता कि "वादा" क्या होता है. आप कंधे उचकाकर आगे बढ़ जाते, लेकिन चमकीले काग़ज़ का वज़न आपके कंधों को झुका रहा है. अगर ऐसा है, तो चिंता न करें. मुझे भी यह समझने में काफ़ी समय लगा कि मुझे इस बारे में क्यों सोचना चाहिए. शायद आपको शुरुआत से शुरू करना है.
- आपने हवा में मुक्का मारा! क्या आपको लगता है कि अब समय आ गया है? आपने पहले भी Promise का इस्तेमाल किया है, लेकिन आपको इस बात से परेशानी होती है कि सभी लागू करने के तरीकों में एपीआई थोड़ा अलग होता है. JavaScript के आधिकारिक वर्शन के लिए एपीआई क्या है? शायद आपको शब्दावली से शुरुआत करनी चाहिए.
- आपको इस बारे में पहले से पता था और आपको उन लोगों पर हंसी आती है जो इस बात पर ऐसे उछल-कूद कर रहे हैं जैसे कि उन्हें यह बात अभी-अभी पता चली हो. अपनी इस उपलब्धि का जश्न मनाएं. इसके बाद, सीधे एपीआई के रेफ़रंस पर जाएं.
ब्राउज़र के साथ काम करना और पॉलीफ़िल
जिन ब्राउज़र में प्रॉमिस को पूरी तरह से लागू नहीं किया गया है उन्हें स्पेसिफ़िकेशन के मुताबिक बनाने के लिए या अन्य ब्राउज़र और Node.js में प्रॉमिस जोड़ने के लिए, पॉलीफ़िल (2k gzipped) देखें.
आखिर यह सब क्या है?
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
});
यह उन इमेज को नहीं पकड़ता जिनमें गड़बड़ी हुई थी. ऐसा इसलिए, क्योंकि हमें उनके बारे में सुनने का मौका नहीं मिला. माफ़ करें, DOM हमें ऐसा करने का कोई तरीका नहीं देता. साथ ही, यह एक इमेज लोड कर रहा है. अगर हमें यह जानना हो कि इमेज का कोई सेट कब लोड हुआ, तो चीज़ें और भी मुश्किल हो जाती हैं.
इवेंट हमेशा सबसे अच्छा तरीका नहीं होते
इवेंट उन चीज़ों के लिए बहुत अच्छे होते हैं जो एक ही ऑब्जेक्ट पर कई बार हो सकती हैं—keyup, touchstart वगैरह. इन इवेंट के साथ, आपको इस बात की परवाह नहीं होती कि लिसनर को अटैच करने से पहले क्या हुआ था. हालांकि, एसिंक्रोनस तरीके से सफलता/असफलता की जानकारी देने के लिए, आपको कुछ ऐसा करना चाहिए:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
प्रॉमिस भी यही काम करते हैं, लेकिन उनका नाम बेहतर होता है. अगर एचटीएमएल इमेज एलिमेंट में "ready" तरीका होता, जो प्रॉमिस दिखाता, तो हम ऐसा कर सकते थे:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
प्रॉमिस, इवेंट लिसनर की तरह ही होते हैं. हालांकि, इनमें ये अंतर होते हैं:
- प्रॉमिस सिर्फ़ एक बार पूरा या अधूरा हो सकता है. यह दो बार सफल या असफल नहीं हो सकता. साथ ही, यह सफल से असफल या असफल से सफल में स्विच नहीं हो सकता.
- अगर कोई प्रॉमिस पूरा हो गया है या पूरा नहीं हुआ है और बाद में आपने पूरा होने/पूरा न होने का कॉलबैक जोड़ा है, तो सही कॉलबैक को कॉल किया जाएगा. भले ही, इवेंट पहले ही हो चुका हो.
यह एसिंक्रोनस तरीके से होने वाली प्रोसेस के सफल/असफल होने पर बेहद काम आता है. इसकी वजह यह है कि आपको इस बात में कम दिलचस्पी होती है कि कोई चीज़ कब उपलब्ध हुई. इसके बजाय, आपको नतीजे पर प्रतिक्रिया देने में ज़्यादा दिलचस्पी होती है.
प्रॉमिस से जुड़ी शब्दावली
डोमेनिक डेनिकोला ने इस लेख के पहले ड्राफ़्ट को पढ़ा और शब्दावली के लिए मुझे "एफ़" ग्रेड दिया. उन्होंने मुझे सज़ा दी और स्टेट्स ऐंड फ़ेट्स को 100 बार लिखने के लिए मजबूर किया. साथ ही, मेरे माता-पिता को एक पत्र लिखा, जिसमें उन्होंने मेरी चिंता ज़ाहिर की. इसके बावजूद, मैं अब भी कई शब्दों को लेकर उलझन में पड़ जाता हूं. हालांकि, यहां कुछ बुनियादी बातें बताई गई हैं:
वादा इनमें से कोई एक हो सकता है:
- fulfilled - प्रॉमिस से जुड़ी कार्रवाई पूरी हो गई है
- rejected - प्रॉमिस से जुड़ी कार्रवाई पूरी नहीं हो सकी
- मंज़ूरी बाकी है - अब तक मंज़ूरी नहीं मिली है या अस्वीकार नहीं किया गया है
- settled - अनुरोध पूरा हो गया है या अस्वीकार कर दिया गया है
The spec
में thenable शब्द का इस्तेमाल, प्रॉमिस जैसे ऑब्जेक्ट के बारे में बताने के लिए भी किया जाता है.
इसमें then तरीका होता है. इस शब्द से मुझे इंग्लैंड की फ़ुटबॉल टीम के पूर्व मैनेजर टेरी वेनेबल्स की याद आती है. इसलिए, मैं इसका इस्तेमाल कम से कम करूंगा.
JavaScript में प्रॉमिस की सुविधा उपलब्ध है!
प्रॉमिस, लाइब्रेरी के तौर पर कुछ समय से मौजूद हैं. जैसे:
ऊपर दिए गए और JavaScript प्रॉमिस, एक जैसे और स्टैंडर्ड तरीके से काम करते हैं. इसे Promises/A+ कहा जाता है. अगर आप jQuery का इस्तेमाल करने वाले उपयोगकर्ता हैं, तो उनके पास Deferreds नाम की मिलती-जुलती चीज़ होती है. हालांकि, Deferreds, Promise/A+ के मुताबिक नहीं होते हैं. इस वजह से, वे थोड़े अलग और कम काम के होते हैं. इसलिए, सावधान रहें. jQuery में Promise टाइप भी होता है. हालांकि, यह Deferred का सिर्फ़ एक सबसेट है और इसमें भी वही समस्याएं होती हैं.
हालांकि, प्रॉमिस को लागू करने का तरीका एक जैसा होता है, लेकिन उनके एपीआई अलग-अलग होते हैं. 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"));
}
});
प्रॉमिस कंस्ट्रक्टर एक आर्ग्युमेंट लेता है. यह दो पैरामीटर वाला कॉलबैक होता है: रिज़ॉल्व और रिजेक्ट. कॉलबैक में कुछ करें, शायद एसिंक्रोनस, फिर सब कुछ काम करने पर रिज़ॉल्व को कॉल करें, नहीं तो रिजेक्ट को कॉल करें.
JavaScript में throw की तरह, Error ऑब्जेक्ट के साथ अस्वीकार करना सामान्य है, लेकिन ज़रूरी नहीं है. गड़बड़ी वाले ऑब्जेक्ट का फ़ायदा यह है कि वे स्टैक ट्रेस को कैप्चर करते हैं. इससे डीबग करने वाले टूल ज़्यादा मददगार बन जाते हैं.
इस वादे को पूरा करने का तरीका यहां बताया गया है:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then() दो आर्ग्युमेंट लेता है. पहला, सफल होने पर कॉलबैक और दूसरा, असफल होने पर कॉलबैक. दोनों ज़रूरी नहीं हैं. इसलिए, सिर्फ़ सफल या असफल होने के मामले के लिए कॉलबैक जोड़ा जा सकता है.
JavaScript प्रॉमिस की शुरुआत डीओएम में "फ़्यूचर" के तौर पर हुई थी. इसके बाद, इसका नाम बदलकर "प्रॉमिस" कर दिया गया. आखिर में, इसे JavaScript में शामिल कर दिया गया. इन्हें डीओएम के बजाय JavaScript में रखना बेहतर होता है, क्योंकि ये Node.js जैसे नॉन-ब्राउज़र जेएस कॉन्टेक्स्ट में उपलब्ध होंगे. हालांकि, यह एक अलग सवाल है कि वे अपने कोर एपीआई में इनका इस्तेमाल करते हैं या नहीं.
हालांकि, ये JavaScript की एक सुविधा हैं, लेकिन डीओएम इनका इस्तेमाल करने से नहीं डरता. दरअसल, एसिंक्रोनस सक्सेस/फ़ेलियर वाले सभी नए डीओएम एपीआई, प्रॉमिस का इस्तेमाल करेंगे. कोटा मैनेजमेंट, फ़ॉन्ट लोड इवेंट, ServiceWorker, वेब एमआईडीआई, स्ट्रीम वगैरह के साथ ऐसा पहले से ही हो रहा है.
अन्य लाइब्रेरी के साथ काम करना
JavaScript Promises API, then() तरीके वाली किसी भी चीज़ को प्रॉमिस-लाइक (या प्रॉमिस-स्पीक sigh में thenable) के तौर पर ट्रीट करेगा. इसलिए, अगर Q प्रॉमिस दिखाने वाली लाइब्रेरी का इस्तेमाल किया जाता है, तो कोई बात नहीं. यह JavaScript के नए प्रॉमिस के साथ अच्छी तरह से काम करेगा.
हालांकि, जैसा कि मैंने बताया, jQuery के Deferreds थोड़े … मददगार नहीं हैं. अच्छी बात यह है कि इन्हें स्टैंडर्ड प्रॉमिस में कास्ट किया जा सकता है. ऐसा जल्द से जल्द करना फ़ायदेमंद होता है:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
यहां, jQuery का $.ajax एक Deferred दिखाता है. इसमें then() तरीका मौजूद है, इसलिए Promise.resolve() इसे JavaScript प्रॉमिस में बदल सकता है. हालांकि, कभी-कभी Deferred ऑब्जेक्ट अपने कॉलबैक को कई आर्ग्युमेंट पास करते हैं. उदाहरण के लिए:
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);
})
हम story.json को तब तक डाउनलोड नहीं करते, जब तक getChapter को कॉल नहीं किया जाता. हालांकि, अगली बार getChapter को कॉल किए जाने पर, हम स्टोरी प्रॉमिस का फिर से इस्तेमाल करते हैं. इसलिए, story.json को सिर्फ़ एक बार फ़ेच किया जाता है. Yay Promises!
गड़बड़ी ठीक करना
जैसा कि हमने पहले देखा था, 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);
})
दोनों में थोड़ा सा अंतर है, लेकिन यह बेहद काम का है. प्रॉमिस रिजेक्शन, रिजेक्शन कॉलबैक (या 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 के तौर पर पार्स करने की कोशिश करता है. साथ ही, यह उस कॉलबैक को भी छोड़ देता है जो पेज में chapter1.html जोड़ता है. इसके बजाय, यह कैच कॉलबैक पर चला जाता है. इस वजह से, अगर पिछली कोई भी कार्रवाई पूरी नहीं हो पाती है, तो पेज पर "चैप्टर नहीं दिखाया जा सका" मैसेज जोड़ दिया जाएगा.
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, एसिंक-अवेयर नहीं है. इसलिए, हमारे चैप्टर डाउनलोड होने के क्रम में दिखेंगे. पल्प फ़िक्शन को इसी तरह लिखा गया था. यह पल्प फ़िक्शन नहीं है, इसलिए चलिए इसे ठीक करते हैं.
कोई क्रम बनाना
हम अपने 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" वैरिएबल की ज़रूरत नहीं होती. अरे में मौजूद हर आइटम के लिए, हमारा 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-style कॉलबैक या इवेंट के साथ ऊपर दिया गया काम करने के लिए, करीब दोगुने कोड की ज़रूरत होती है. हालांकि, इससे भी अहम बात यह है कि इसे समझना आसान नहीं है. हालांकि, प्रॉमिस के बारे में इतना ही नहीं है. जब इन्हें ES6 की अन्य सुविधाओं के साथ इस्तेमाल किया जाता है, तो ये और भी आसान हो जाते हैं.
बोनस राउंड: ज़्यादा सुविधाएं
जब मैंने मूल रूप से यह लेख लिखा था, तब से Promises का इस्तेमाल करने की क्षमता काफ़ी बढ़ गई है. Chrome 55 के बाद से, एसिंक्रोनस फ़ंक्शन की मदद से प्रॉमिस-आधारित कोड को सिंक्रोनस कोड की तरह लिखा जा सकता है. हालांकि, इसके लिए मुख्य थ्रेड को ब्लॉक नहीं किया जाता. इस बारे में ज़्यादा जानने के लिए, एसिंक्रोनस फ़ंक्शन के बारे में मेरा लेख पढ़ें. मुख्य ब्राउज़र में, Promises और async फ़ंक्शन, दोनों का बड़े पैमाने पर इस्तेमाल किया जाता है. आपको एमडीएन के प्रॉमिस और एसिंक फ़ंक्शन रेफ़रंस में जानकारी मिल सकती है.
ऐन वैन केस्टरेन, डॉमनिक डेनिकोला, टॉम ऐशवर्थ, रेमी शार्प, ऐडी उस्मानी, आर्थर इवांस, और युताका हिरानो को बहुत-बहुत धन्यवाद. इन्होंने इस लेख को प्रूफ़रीड किया और इसमें सुधार किए/सुझाव दिए.
साथ ही, लेख के अलग-अलग हिस्सों को अपडेट करने के लिए, मैथियास बायनेंस को धन्यवाद.