JavaScript Promises: परिचय

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

जेक आर्चिबाल्ड
जेक आर्चिबाल्ड

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

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

JavaScript में वादे अब उपलब्ध हैं!

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

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

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

ब्राउज़र सपोर्ट और पॉलीफ़िल

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

  • 32
  • 12
  • 29
  • 8

सोर्स

जिन ब्राउज़र में नीतियों के पालन से जुड़ी कोई गारंटी नहीं दी गई है उन्हें वापस लाने के लिए या दूसरे ब्राउज़र और Node.js से वादों को जोड़ने के लिए, पॉलीफ़िल (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
});

यह ऐसी इमेज नहीं पकड़ता जिनमें हमें उन्हें सुनने का मौका मिलने से पहले गड़बड़ी हुई थी; माफ़ करें, DOM हमें ऐसा करने का तरीका नहीं बताता. साथ ही, यह एक इमेज लोड हो रहा है. अगर हम यह जानना चाहें कि इमेज का सेट कब लोड होता है, तो यह मामला और भी पेचीदा हो जाता है.

इवेंट हमेशा बेहतर तरीके से नहीं होते

इवेंट, एक ही ऑब्जेक्ट पर कई बार हो सकते हैं, जैसे कि 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 बार राज्य और किस्मत की बात कॉपी करने के लिए मजबूर किया और अपने माता-पिता को एक चिट्ठी लिखी. इसके बावजूद, मुझे अब भी काफ़ी शब्दावली मिली हुई है, लेकिन बुनियादी बातें यहां बताई गई हैं:

वादा इनमें से कोई भी हो सकता है:

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

इस खास जानकारी में भी 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 में ले जाया गया. इन्हें DOM के बजाय JavaScript में रखना अच्छा होता है, क्योंकि वे बिना ब्राउज़र वाले JS कॉन्टेक्स्ट में उपलब्ध रहेंगे, जैसे कि Node.js (क्या वे अपने मुख्य एपीआई में उनका इस्तेमाल करते हैं या नहीं). यह एक और सवाल है.

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

दूसरी लाइब्रेरी के साथ काम करने की सुविधा

JavaScript प्रॉमिस एपीआई हर बात को then() तरीके से प्रॉमिस-जैसे (या thenable प्रॉमिस-स्पीक sigh) के तौर पर इस्तेमाल करेगा. इसलिए, अगर कोई ऐसी लाइब्रेरी का इस्तेमाल किया जाता है जो 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);
})

यह अंतर बहुत मामूली है, लेकिन बहुत काम का है. प्रॉमिस अस्वीकार करने पर, अस्वीकार करने वाले कॉलबैक या catch() का इस्तेमाल करके, अगले then() पर जाएं. then(func1, func2) होने पर, func1 या func2 कॉल होंगे, दोनों नहीं. हालांकि, अगर func1 अस्वीकार कर देता है, तो then(func1).catch(func2) के साथ दोनों को कॉल किया जाएगा. ऐसा इसलिए, क्योंकि चेन में ये चरण अलग-अलग हैं. साथ ही, ये भी काम करें:

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

क्रम बनाया जा रहा है

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

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

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

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

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