প্রমিস, ডিফার্ড ও অ্যাসিঙ্ক্রোনাস কম্পিউটেশনকে সহজ করে তোলে। প্রমিস এমন একটি অপারেশনকে দেখায় যা এখনও সম্পূর্ণ হয়নি।
ডেভেলপাররা, ওয়েব ডেভেলপমেন্টের ইতিহাসে একটি গুরুত্বপূর্ণ মুহূর্তের জন্য নিজেকে প্রস্তুত করুন।
[ড্রামরোল শুরু হয়]
জাভাস্ক্রিপ্টে প্রমিস এসে গেছে!
[আতশবাজি ফাটছে, উপর থেকে চকচকে কাগজ ঝরে পড়ছে, ভিড় উন্মত্ত হয়ে উঠেছে]
এই পর্যায়ে আপনি এইসব বিভাগের মধ্যে কোনও একটির মধ্যে পড়েন:
- আপনার আশেপাশের লোকজন উল্লাস করছেন, কিন্তু আপনি নিশ্চিত নন যে কী নিয়ে এত উত্তেজনা। হয়ত তুমি জানোই না "প্রতিশ্রুতি" কী। তুমি কাঁধ ঝাঁকাবে, কিন্তু চকচকে কাগজের ওজন তোমার কাঁধকে ভারাক্রান্ত করে তুলবে। যদি তাই হয়, তাহলে চিন্তা করবেন না, এই ধরনের জিনিস নিয়ে কেন মাথা ঘামানো উচিত তা বুঝতে আমার অনেক সময় লেগেছে। আপনি সম্ভবত শুরু থেকে শুরু করতে চান।
- আপনি আনন্দে লাফিয়ে ওঠেন! সময় হয়ে গেছে, তাই না? আপনি আগে এই Promise জিনিসগুলি ব্যবহার করেছেন কিন্তু এটি আপনাকে বিরক্ত করে যে সমস্ত ইমপ্লিমেন্টেশনের একটি সামান্য ভিন্ন API আছে। অফিসিয়াল জাভাস্ক্রিপ্ট ভার্সনের জন্য API কী? আপনি সম্ভবত পরিভাষা দিয়ে শুরু করতে চান।
- আপনি আগে থেকেই এই বিষয়ে জানতেন এবং যারা এই খবর শুনে আনন্দে লাফালাফি করছে তাদের আপনি উপহাস করেন। নিজের শ্রেষ্ঠত্বে গর্বিত হওয়ার জন্য একটু সময় নিন, তারপর সোজা API রেফারেন্স-এ যান।
ব্রাউজার সাপোর্ট ও পলিফিল
যেসব ব্রাউজারে সম্পূর্ণ প্রমিস ইমপ্লিমেন্টেশন নেই সেগুলিকে স্পেক কমপ্লায়েন্সের উপযোগী করে তুলতে অথবা অন্যান্য ব্রাউজার ও Node.js-এ প্রমিস যোগ করতে, পলিফিল (2k gzipped) চেক করে দেখুন।
এত হইচই কীসের?
জাভাস্ক্রিপ্ট হল সিঙ্গল থ্রেডেড, যার অর্থ হল স্ক্রিপ্টের দুটি বিট একই সময়ে রান করতে পারে না; সেগুলি একটির পর একটি রান করতে হয়। ব্রাউজারে, জাভাস্ক্রিপ্ট অন্যান্য অনেক কিছুর সাথে থ্রেড শেয়ার করে যা ব্রাউজার থেকে ব্রাউজারে আলাদা হয়। কিন্তু সাধারণত জাভাস্ক্রিপ্ট, পেইন্টিং, স্টাইল আপডেট করা এবং ব্যবহারকারীর অ্যাকশন (যেমন টেক্সট হাইলাইট করা এবং ফর্ম কন্ট্রোলের সাথে ইন্টার্যাক্ট করা) ম্যানেজ করার মতো একই সারিতে থাকে। এর মধ্যে কোনও একটি অ্যাক্টিভিটি করলে অন্যগুলি করতে দেরি হয়।
মানুষ হিসেবে আপনি মাল্টিথ্রেডেড। আপনি একাধিক আঙুল দিয়ে টাইপ করতে পারেন, আপনি একই সাথে গাড়ি চালাতে ও কথোপকথন চালিয়ে যেতে পারেন। আমাদের একমাত্র যে ব্লকিং ফাংশন নিয়ে কাজ করতে হয় তা হল হাঁচি, যেখানে হাঁচির সময়সীমার জন্য সমস্ত বর্তমান অ্যাক্টিভিটি অবশ্যই সাসপেন্ড করতে হবে। এটি খুবই বিরক্তিকর, বিশেষ করে যখন আপনি গাড়ি চালাচ্ছেন এবং কথোপকথন চালিয়ে যাওয়ার চেষ্টা করছেন। আপনি এমন কোড লিখতে চান না যা স্নিজি।
আপনি সম্ভবত এটি এড়াতে ইভেন্ট এবং কলব্যাক ব্যবহার করেছেন। এখানে ইভেন্টগুলি দেওয়া হল:
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
});
এটিই হল প্রমিজের কাজ, তবে আরও ভাল নাম সহ। HTML ইমেজ এলিমেন্টে যদি "ready" মেথড থাকত যা প্রমিস রিটার্ন করত, তাহলে আমরা এটি করতে পারতাম:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
সবচেয়ে মৌলিক স্তরে, প্রমিসগুলি ইভেন্ট লিসনারের মতো, তবে কিছু পার্থক্য রয়েছে:
- একটি প্রমিস শুধুমাত্র একবারই সফল বা ব্যর্থ হতে পারে। এটি দুবার সফল বা ব্যর্থ হতে পারে না, এটি সফল থেকে ব্যর্থ বা ব্যর্থ থেকে সফল হতে পারে না।
- কোনও প্রমিস সফল বা ব্যর্থ হলে এবং আপনি পরে কোনও সাফল্য/ব্যর্থতার কলব্যাক যোগ করলে, সঠিক কলব্যাকটি কল করা হবে, যদিও ইভেন্টটি আগে ঘটেছে।
এটি অ্যাসিঙ্ক সাফল্য/ব্যর্থতার জন্য অত্যন্ত উপযোগী, কারণ আপনি কোনও কিছু উপলভ্য হওয়ার সঠিক সময় সম্পর্কে কম আগ্রহী এবং ফলাফলের উপর প্রতিক্রিয়া জানাতে বেশি আগ্রহী।
প্রতিশ্রুতি সংক্রান্ত পরিভাষা
Domenic Denicola এই নিবন্ধের প্রথম ড্রাফ্ট প্রুফ রিড করেছেন এবং পরিভাষার জন্য আমাকে "F" গ্রেড দিয়েছেন। তিনি আমাকে ডিটেনশনে রেখেছিলেন, আমাকে জোর করে স্টেটস অ্যান্ড ফেটস ১০০ বার কপি করতে বাধ্য করেছিলেন এবং আমার বাবা-মাকে একটি চিন্তিত চিঠি লিখেছিলেন। তা সত্ত্বেও, আমি এখনও অনেক পরিভাষা গুলিয়ে ফেলি, তবে এখানে প্রাথমিক বিষয়গুলি দেওয়া হল:
প্রতিশ্রুতি এগুলি হতে পারে:
- পূরণ করা হয়েছে - প্রতিশ্রুতি সম্পর্কিত অ্যাকশনটি সফল হয়েছে
- বাতিল করা হয়েছে - প্রতিশ্রুতি সম্পর্কিত অ্যাকশনটি করা যায়নি
- বাকি আছে - এখনও পূরণ বা বাতিল করা হয়নি
- নিষ্পত্তি করা হয়েছে - পূরণ বা বাতিল করা হয়েছে
স্পেক
এছাড়াও thenable শব্দটি ব্যবহার করে এমন একটি অবজেক্ট বর্ণনা করে যা প্রমিস-লাইক,
এতে একটি then পদ্ধতি রয়েছে। এই শব্দটা শুনলে আমার ইংল্যান্ড ফুটবল টিমের প্রাক্তন ম্যানেজার টেরি ভেনাবেলস-এর কথা মনে পড়ে যায়, তাই
আমি এই শব্দটা যতটা সম্ভব কম ব্যবহার করব।
জাভাস্ক্রিপ্টে প্রমিস এসে গেছে!
লাইব্রেরির আকারে প্রমিস অনেকদিন ধরেই আছে, যেমন:
উপরের এবং JavaScript প্রমিস একটি সাধারণ, স্ট্যান্ডার্ড আচরণ শেয়ার করে যাকে Promises/A+ বলা হয়। আপনি jQuery ব্যবহারকারী হলে, তাদের কাছে Deferreds নামে একই রকম কিছু আছে। তবে, Deferreds, Promise/A+ মেনে চলে না, যার ফলে সেগুলি সামান্য আলাদা এবং কম উপযোগী, তাই সাবধান। jQuery-তেও একটি Promise টাইপ আছে, তবে এটি Deferred-এর সাবসেট এবং এতে একই সমস্যা আছে।
যদিও প্রমিস ইমপ্লিমেন্টেশন একটি স্ট্যান্ডার্ড আচরণ অনুসরণ করে, তবে তাদের সামগ্রিক API আলাদা। জাভাস্ক্রিপ্ট প্রমিস API-এর ক্ষেত্রে 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"));
}
});
প্রমিস কনস্ট্রাক্টর একটি আর্গুমেন্ট নেয়, দুটি প্যারামিটার সহ একটি কলব্যাক, সমাধান এবং প্রত্যাখ্যান। কলব্যাকের মধ্যে কিছু করুন, সম্ভবত অ্যাসিঙ্ক, তারপর সবকিছু কাজ করলে সমাধান কল করুন, অন্যথায় প্রত্যাখ্যান কল করুন।
সাধারণ পুরনো জাভাস্ক্রিপ্টে throw-এর মতো, এটি প্রথাগত, তবে Error অবজেক্টের মাধ্যমে
বাতিল করার প্রয়োজন নেই। Error অবজেক্টের সুবিধা হল, এটি স্ট্যাক ট্রেস ক্যাপচার করে,
যার ফলে ডিবাগিং টুল আরও সহায়ক হয়ে ওঠে।
আপনি কীভাবে সেই প্রতিশ্রুতি ব্যবহার করবেন তা এখানে দেওয়া হল:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()-এর দুটি আর্গুমেন্ট আছে, একটি সফল হওয়ার ক্ষেত্রে কলব্যাক এবং অন্যটি
ব্যর্থ হওয়ার ক্ষেত্রে। দুটিই ঐচ্ছিক, তাই আপনি শুধুমাত্র সফল বা ব্যর্থতার ক্ষেত্রে
কলব্যাক যোগ করতে পারেন।
জাভাস্ক্রিপ্ট প্রমিস DOM-এ "Futures" হিসেবে শুরু হয়েছিল, পরে নাম পরিবর্তন করে "Promises" করা হয়, এবং অবশেষে জাভাস্ক্রিপ্টে সরানো হয়। DOM-এর পরিবর্তে জাভাস্ক্রিপ্টে এগুলি থাকলে খুব ভালো হয় কারণ Node.js-এর মতো নন-ব্রাউজার JS প্রসঙ্গে এগুলি উপলভ্য হবে (কোর API-তে এগুলি ব্যবহার করা হবে কিনা তা অন্য প্রশ্ন)।
যদিও তারা একটি জাভাস্ক্রিপ্ট ফিচার, DOM তাদের ব্যবহার করতে ভয় পায় না। আসলে, অ্যাসিঙ্ক সাফল্য/ব্যর্থতা পদ্ধতি সহ সব নতুন DOM API প্রমিস ব্যবহার করবে। এটি ইতিমধ্যেই কোটা ম্যানেজমেন্ট, ফন্ট লোড ইভেন্ট, ServiceWorker, Web MIDI, স্ট্রিম এবং আরও অনেক কিছুর সাথে ঘটছে।
অন্যান্য লাইব্রেরির সাথে মানানসই
JavaScript প্রমিস API, then() পদ্ধতি সহ যেকোনও কিছুকে প্রমিসের মতো (বা প্রমিস-স্পিকে thenable দীর্ঘশ্বাস) হিসেবে বিবেচনা করবে, তাই আপনি যদি এমন একটি লাইব্রেরি ব্যবহার করেন যা একটি Q প্রমিস রিটার্ন করে, তাহলে ঠিক আছে, এটি নতুন JavaScript প্রমিসের সাথে ভালভাবে কাজ করবে।
যদিও, আমি যেমন উল্লেখ করেছি, jQuery-এর Deferreds কিছুটা … সহায়ক নয়। তবে, আপনি এগুলিকে স্ট্যান্ডার্ড প্রমিজে কাস্ট করতে পারেন, যা যত তাড়াতাড়ি সম্ভব করা উচিত:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
এখানে, jQuery-এর $.ajax একটি Deferred রিটার্ন করে। এতে then() পদ্ধতি আছে বলে,
Promise.resolve() এটিকে জাভাস্ক্রিপ্ট প্রমিজে পরিণত করতে পারে। তবে,
কখনও কখনও ডেফার্ড তার কলব্যাকে একাধিক আর্গুমেন্ট পাস করে, যেমন:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
যেখানে JS প্রমিস প্রথমটি ছাড়া বাকি সব উপেক্ষা করে:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
সাধারণত আপনি যা চান, এটি তাই করে অথবা অন্তত আপনি যা চান তা অ্যাক্সেস করার সুবিধা দেয়। এছাড়াও, মনে রাখবেন যে jQuery, রিজেকশনে Error অবজেক্ট পাস করার কনভেনশন অনুসরণ করে না।
জটিল অ্যাসিঙ্ক কোড আরও সহজ করা হয়েছে
ঠিক আছে, কিছু কোড করা যাক। ধরা যাক, আমরা চাই:
- লোডিং দেখানোর জন্য স্পিনার চালু করুন
- একটি স্টোরির জন্য কিছু JSON ফেচ করো, যা আমাদের প্রতিটি চ্যাপ্টারের জন্য শীর্ষক এবং URL দেয়
- পৃষ্ঠায় শীর্ষক যোগ করুন
- প্রতিটি চ্যাপ্টার ফেচ করুন
- পৃষ্ঠায় গল্প যোগ করুন
- স্পিনার বন্ধ করুন
… তবে, কোনও সমস্যা হলে ব্যবহারকারীকে তা জানান। আমরাও সেই পয়েন্টে স্পিনার বন্ধ করতে চাইব, অন্যথায় এটি ঘুরতেই থাকবে, মাথা ঘুরবে এবং অন্য কোনও UI-তে ক্র্যাশ করবে।
অবশ্যই, আপনি কোনও গল্প ডেলিভার করার জন্য JavaScript ব্যবহার করবেন না, HTML হিসেবে পরিবেশন করা দ্রুততর, কিন্তু API-এর সাথে ডিল করার সময় এই প্যাটার্নটি বেশ সাধারণ: একাধিক ডেটা ফেচ করা, তারপর সব হয়ে গেলে কিছু করা।
শুরু করার জন্য, নেটওয়ার্ক থেকে ডেটা ফেচ করার বিষয়টি দেখা যাক:
XMLHttpRequest-কে প্রমিস করা হচ্ছে
ব্যাকওয়ার্ড কম্প্যাটিবল উপায়ে সম্ভব হলে, প্রমিস ব্যবহার করার জন্য পুরনো API আপডেট করা হবে। 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 টাইপ না করেই HTTP অনুরোধ করতে পারি, যা খুবই ভালো, কারণ
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() এখনও একটি প্রমিস রিটার্ন করে, যা একটি URL ফেচ করে তারপর
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-এ একটি অ্যাসিঙ্ক অনুরোধ করি, যা আমাদের অনুরোধ করার জন্য URL-এর একটি সেট দেয়, তারপর আমরা সেগুলির মধ্যে প্রথমটির জন্য অনুরোধ করি। This is when promises
really start to stand out from simple callback patterns.
এমনকি আপনি চ্যাপ্টার পাওয়ার জন্য একটি শর্টকাট পদ্ধতিও তৈরি করতে পারেন:
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(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!");
})
উপরের ফ্লোটি সাধারণ জাভাস্ক্রিপ্ট try/catch-এর মতোই, "try"-এর মধ্যে হওয়া সমস্যা
সরাসরি catch() ব্লকে চলে যায়। উপরে যা বলা হয়েছে, তা ফ্লোচার্ট হিসেবে এখানে দেওয়া হল (কারণ আমি ফ্লোচার্ট পছন্দ করি):
যেসব প্রতিশ্রুতি পূরণ করা হয়েছে সেগুলি নীল লাইন দিয়ে এবং যেগুলি পূরণ করা হয়নি সেগুলি লাল লাইন দিয়ে দেখানো হয়েছে।
জাভাস্ক্রিপ্ট এক্সেপশন ও প্রমিস
প্রমিস স্পষ্টভাবে বাতিল করা হলে, বাতিল করার ঘটনা ঘটে, তবে কনস্ট্রাক্টর কলব্যাকে কোনও সমস্যা হলে, অস্পষ্টভাবেও বাতিল করার ঘটনা ঘটে:
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] ফেচ করা না গেলে (যেমন, http 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';
})
কিন্তু আমরা কীভাবে চ্যাপ্টারের URL-এর মাধ্যমে লুপ করতে পারি এবং সেগুলি ক্রমানুসারে আনতে পারি? এটি কাজ করছে না:
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())
এটি আগের উদাহরণের মতোই কাজ করছে, তবে আলাদা "sequence" ভেরিয়েবলের প্রয়োজন নেই। অ্যারের প্রতিটি আইটেমের জন্য আমাদের রিডিউস কলব্যাক কল করা হয়।
প্রথমবার "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';
})
এবং আমাদের কাছে সিঙ্ক ভার্সনের সম্পূর্ণ অ্যাসিঙ্ক ভার্সন আছে। কিন্তু আমরা আরও ভালো করতে পারি। এই মুহূর্তে আমাদের পৃষ্ঠাটি এইভাবে ডাউনলোড হচ্ছে:
ব্রাউজার একসাথে একাধিক জিনিস ডাউনলোড করার ক্ষেত্রে বেশ ভালো, তাই আমরা একের পর এক চ্যাপ্টার ডাউনলোড করে পারফর্ম্যান্স হারাচ্ছি। আমরা যা করতে চাই তা হল একই সময়ে সেগুলি সব ডাউনলোড করা, তারপর সেগুলি সব এসে গেলে প্রসেস করা। ভাগ্যিস, এর জন্য একটি API আছে:
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 এবং অ্যাসিঙ্ক ফাংশন দুটির জন্যই ব্যাপক সমর্থন রয়েছে। আপনি MDN-এর Promise এবং async ফাংশন রেফারেন্সে বিবরণ দেখতে পাবেন।
অ্যান ভ্যান কেস্টেরেন, ডোমেনিক ডেনিকোলা, টম অ্যাশওয়ার্থ, রেমি শার্প, অ্যাডি ওসমানি, আর্থার ইভান্স এবং ইউতাকা হিরানোকে অনেক ধন্যবাদ যারা এটি প্রুফরিড করেছেন এবং সংশোধন/সুপারিশ করেছেন।
এছাড়াও, Mathias Bynens-কে ধন্যবাদ, নিবন্ধের বিভিন্ন অংশ আপডেট করার জন্য।