JavaScript Promises: परिचय

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

डेवलपर, वेब डेवलपमेंट के इतिहास के एक अहम पल के लिए खुद को तैयार करें.

[ड्रमरोल की शुरुआत]

JavaScript में वादा पूरा हो गया है!

[पटाखे फोड़ते हैं, ऊपर से चमचमाते काग़ज़ की बारिश होती है, भीड़ का सामना करना पड़ता है]

इस समय, आप इनमें से किसी एक कैटगरी में आते हैं:

  • लोग आपके आस-पास खुश कर रहे हैं, लेकिन आपको पता नहीं है कि यह परेशानी क्या है. शायद आपको यह भी पता नहीं कि "प्रॉमिस" क्या होता है. आप कंधे उचकाएंगे, लेकिन चमचमाते हुए पेपर का वज़न आपके कंधों पर तो भारी पड़ रहा है. अगर हां, तो इसकी चिंता न करें, यह पता लगाने में मुझे कई उम्र लग गई कि मुझे इस चीज़ों की परवाह क्यों करनी चाहिए. आप शायद शुरुआत से इसे शुरू करना चाहें.
  • आपने हवा में एक छलांग लगाई है! समय के बारे में सही जानकारी? आपने पहले इन Promise चीज़ों का इस्तेमाल किया है, लेकिन आपको इस बात से परेशानी है कि हर ऐप्लिकेशन में लागू होने वाला एपीआई थोड़ा अलग होता है. JavaScript के आधिकारिक वर्शन का एपीआई क्या है? ऐसा हो सकता है कि आप शब्दावली से शुरुआत करना चाहें.
  • आपको यह पहले से पता था और आप ऊपर-नीचे होने वालों का मज़ाक़ उड़ाने वाले ऐसे लोगों का मज़ाक़ उड़ाते हैं जो उनके लिए खबर हैं. कुछ समय निकालकर खुद की श्रेष्ठता का लुत्फ़ उठाएं, फिर सीधे एपीआई के संदर्भ की ओर बढ़ें.

ब्राउज़र सहायता और पॉलीफ़िल

ब्राउज़र सहायता

  • 32
  • 12
  • 29
  • 8

सोर्स

जिन ब्राउज़र में पूरा प्रॉमिस नहीं है उन्हें स्पेसिफ़िकेशन के हिसाब से, सही तरीके से लागू करने या दूसरे ब्राउज़र और Node.js के लिए प्रॉमिस को जोड़ने के लिए, polyfill (2k gzip) देखें.

ये सब किस बारे में है?

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 तब तक एक्ज़ीक्यूट नहीं करता, जब तक उन लिसनर को कॉल नहीं किया जाता.

माफ़ करें, ऊपर दिए गए उदाहरण में, ऐसा हो सकता है कि ये इवेंट हमारे सुनने से पहले हुए हों. इसलिए, हमें इमेज की "पूरी" प्रॉपर्टी का इस्तेमाल करके इस पर काम करना होगा:

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

मूल रूप से, वादे इवेंट लिसनर जैसे कुछ होते हैं, इन्हें छोड़कर:

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

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

प्रोमिस शब्दावली

Domenicdenica प्रूफ़ की मदद से, इस लेख का पहला ड्राफ़्ट पढ़ा गया और शब्दावली में मुझे "F" ग्रेड दिया गया. उसने मुझे कारावास में डाल दिया, मुझे राज्य और उसके हालात को 100 बार कॉपी करने के लिए मजबूर किया, और मेरे माता-पिता को चिल्लाने के लिए एक चिट्ठी भी लिखी. इसके बावजूद, मुझे अब भी बहुत सारी शब्दावली मिलती-जुलती हैं, लेकिन ये रही बुनियादी बातें:

प्रॉमिस:

  • fullfill - प्रॉमिस से जुड़ी कार्रवाई पूरी हो गई
  • rejected - प्रॉमिस से जुड़ी कार्रवाई पूरी नहीं हो सकी
  • मंज़ूरी बाकी है - उसे अभी तक पूरा या अस्वीकार नहीं किया गया है
  • सेटल किया गया - उसे पूरा या अस्वीकार किया गया है

इस स्पेसिफ़िकेशन में, प्रॉमिस जैसे दिखने वाले ऑब्जेक्ट के बारे में बताने के लिए, thenable शब्द का इस्तेमाल किया जाता है. इसमें then वाला तरीका होता है. यह शब्द मुझे इंग्लैंड के पूर्व फ़ुटबॉल मैनेजर टेरी वेनेबल्स की याद दिलाता है इसलिए, मैं इसका इस्तेमाल कम से कम करूंगा.

प्रॉमिस JavaScript में उपलब्ध होता है!

वादों को लाइब्रेरी के रूप में लंबे समय से इस्तेमाल किया जा रहा है, जैसे कि:

ऊपर दिए गए और JavaScript के वादों में एक सामान्य और स्टैंडर्ड बिहेवियर शेयर किया जाता है. जिसे Promises/A+ कहा जाता है. अगर आप jQuery के उपयोगकर्ता हैं, तो इनमें कुछ ऐसा मिलता-जुलता है डिफ़र्ड. हालांकि, स्थगित की सुविधा Promise/A+ का पालन नहीं करती है, जिसकी वजह से वे पूरी तरह से अलग और कम उपयोगी होती हैं. इसलिए, सावधान रहें. jQuery में प्रॉमिस टाइप भी है, लेकिन यह सिर्फ़ 'स्थगित' का एक सबसेट है और उसमें वही समस्याएं हैं.

हालांकि, प्रॉमिस लागू करने के तरीके स्टैंडर्ड के मुताबिक होते हैं, लेकिन उनके सभी एपीआई अलग-अलग होते हैं. 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"));
  }
});

प्रॉमिस कंस्ट्रक्टर, एक आर्ग्युमेंट, दो पैरामीटर वाला कॉलबैक, और इसे हल करता है. कॉलबैक में कुछ काम करें, जैसे कि एक साथ काम नहीं करने वाली प्रोसेस. इसके बाद, अगर सब कुछ काम करे, तो रिज़ॉल्व को कॉल करें. अगर ऐसा नहीं है, तो कॉल को अस्वीकार कर दें.

सामान्य पुराने JavaScript के throw की तरह, यह आम तौर पर इस्तेमाल किया जाता है, लेकिन गड़बड़ी ऑब्जेक्ट के साथ अस्वीकार करना ज़रूरी नहीं है. गड़बड़ी वाले ऑब्जेक्ट का फ़ायदा यह है कि वे स्टैक ट्रेस कैप्चर करते हैं. इससे, डीबग करने वाले टूल ज़्यादा मददगार बन जाते हैं.

यहां बताया गया है कि इस प्रॉमिस का इस्तेमाल कैसे किया जाता है:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

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

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

हालांकि, ये JavaScript की सुविधा होती है, लेकिन DOM उनका इस्तेमाल करने से डरता नहीं है. असल में, एक साथ काम न करने वाले या फ़ेल होने के तरीकों वाले सभी नए डीओएम एपीआई, प्रॉमिस का इस्तेमाल करेंगे. कोटा मैनेजमेंट, फ़ॉन्ट लोड इवेंट, ServiceWorker, वेब एमआईडीआई, और स्ट्रीम वगैरह के साथ पहले से ही ऐसा हो रहा है.

अन्य लाइब्रेरी के साथ काम करना

JavaScript प्रॉमिस एपीआई, then() तरीके के साथ किसी भी चीज़ को प्रॉमिस जैसा प्रोसेस करेगा (या प्रॉमिस-स्पीक आह में thenable), इसलिए अगर आपने ऐसी लाइब्रेरी का इस्तेमाल किया है जो Q प्रॉमिस दिखाती है, तो कोई बात नहीं. यह JavaScript के नए प्रॉमिस के साथ अच्छी तरह से काम करेगा.

हालांकि, जैसा कि मैंने बताया, jQuery के डिफ़र्ड नहीं हैं ... काम के नहीं हैं. शुक्र है कि आप उन्हें सामान्य वादों में कास्ट कर सकते हैं, जो जल्द से जल्द करने लायक है:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

यहां, jQuery का $.ajax 'स्थगित' दिखाता है. इसमें 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 गड़बड़ी वाले ऑब्जेक्ट को अस्वीकार करने की प्रोसेस का पालन नहीं करता है.

जटिल एसिंक कोड अब आसान हो गया है

चलिए, कुछ चीज़ों के लिए कोड डालते हैं. मान लें कि हम:

  1. लोड होने के बारे में बताने के लिए, स्पिनर शुरू करें
  2. किसी कहानी के लिए कुछ JSON फ़ेच करें, जिनसे हमें हर चैप्टर का टाइटल और यूआरएल मिलते हैं
  3. पेज पर टाइटल जोड़ें
  4. हर चैप्टर फ़ेच करें
  5. पेज पर कहानी जोड़ें
  6. स्पिनर रोकें

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

बेशक, आपको किसी स्टोरी को डिलीवर करने के लिए 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);
})

हम story.json को तब तक डाउनलोड नहीं करते, जब तक getChapter को कॉल नहीं किया जाता. हालांकि, अगली बार 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(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 के सामान्य 'कोशिश करें/कैच करें' जैसा ही है. "कोशिश करें" में होने वाली गड़बड़ियां तुरंत 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);
})

गड़बड़ी को ठीक करना

अपनी कहानी और चैप्टर के साथ, हम उपयोगकर्ता को कोई गड़बड़ी दिखाने के लिए कैच करने की सुविधा का इस्तेमाल कर सकते हैं:

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 को जोड़ने वाले कॉलबैक को भी स्किप कर देता है. इसके बजाय, यह कैच कॉलबैक पर जाता है. अगर पिछली कोई भी कार्रवाई पूरी नहीं हो पाती है, तो "चैप्टर नहीं दिखाया जा सका" पेज पर जोड़ दिया जाएगा.

JavaScript के 'आज़माएं/कैच करें' की तरह, गड़बड़ी का पता चलता है और उसके बाद का कोड जारी रहता है. इसलिए, स्पिनर हमेशा छिपा रहता है और हमें इसी की ज़रूरत होती है. ऊपर दिया गया वर्शन, ब्लॉक न होने वाला एसिंक्रोनस वर्शन बन जाता है:

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 फ़िक्शन लिखा गया था. यह पल्प फ़िक्शन नहीं है, इसलिए चलिए इसे ठीक करते हैं.

क्रम बनाना

हमें अपने 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'), यह ऐसा वादा करता है जो उस वैल्यू को पूरा करता है. अगर इसे बिना किसी वैल्यू के कॉल किया जाता है, तो यह "तय नहीं है" से पूरा करता है.

एक 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())

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

बोनस राउंड: ज़्यादा सुविधाएं

जब से मैंने इस लेख को लिखा था, तब से प्रॉमिसेस का इस्तेमाल करने की सुविधा बहुत अच्छी हो गई है. Chrome 55 के बाद से, एक साथ काम नहीं करने वाले (एसिंक) फ़ंक्शन ने प्रॉमिस-आधारित कोड को इस तरह लिखने दिया है जैसे कि वह सिंक्रोनस था, लेकिन वह मुख्य थ्रेड को ब्लॉक किए बिना लिखता था. इस बारे में ज़्यादा जानने के लिए, my async functions article पर जाएं. प्रमुख ब्राउज़र में Promises और एक साथ काम नहीं करने वाली सुविधा, दोनों के लिए बड़े पैमाने पर सहायता उपलब्ध है. इसकी जानकारी एमडीएन के प्रॉमाइज़ और एक साथ काम करने वाले फ़ंक्शन के रेफ़रंस में देखी जा सकती है.

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

साथ ही, इस लेख के अलग-अलग हिस्सों को अपडेट करने के लिए, मैथियास बायनेन्स को धन्यवाद.