5.2 কিভাবে অ্যাসিঙ্ক্রোনাস জাভাস্ক্ৰিপ্ট হ্যান্ডেল করবেন

 

জাভাস্ক্রিপ্টে অ্যাসিঙ্ক্রোনাস কোড লেখার তিনটি উপায় আছে, কলব্যাক, প্রমিজ এবং অ্যাসিঙ্ক/এওয়েট। চলুন একটি একটি করে আলোচনা করা যাক।

কলব্যাক:

কলব্যাক ফাংশনের মানে এক কথায় এটা এমন একটা ফাংশন যেটা আরেকটা ফাংশন এক্সিকিউট হওয়ার পর এক্সিকিউট হয়। আর এজন্যেই এটার নাম কলব্যাক ফাংশন। আমরা জাভাস্ক্রিপ্টে Higher Order Function এর কথা জানি। যেখানে একটা ফাংশন আরেকটা ফাংশন return করতে পারে আবার একই সাথে একটা ফাংশন আর্গুমেন্ট হিসেবে আরেকটা ফাংশনকেও নিতে পারে। যখনি এরকম একটা ফাংশন আরেকটা ফাংশনকে আর্গুমেন্ট হিসেবে নেয়, তখনি আমরা সেই আর্গুমেন্টে যাওয়া ফাংশনটাকে কলব্যাক ফাংশন বলি।

মাঝেমধ্যে আমরা আমাদের কোডের সিকুয়েন্স নিয়ন্ত্রণ করতে চাই, তখন আমরা খুব সহজেই কলব্যাক ফাংশন ব্যবহার করতে পারি। নিচের কোডটি খেয়াল করি, আমরা কলব্যাক ফাংশন ব্যবহার করে খুব সহজেই আগে যোগ করে তারপর আউটপুট দেখাচ্ছি।

function myDisplayer(some) {
  document.getElementById("demo").innerHTML = some;
}

function myCalculator(num1, num2) {
  let sum = num1 + num2;
  return sum;
}

let result = myCalculator(5, 5);
myDisplayer(result);

খুবই সহজ একটি উদাহরণ। কিন্তু কলব্যাকের মুল কাজ হল জাভাস্ক্রিপ্টের অ্যাসিনক্রোনাস আচরণ নিয়ন্ত্রণ করা। আমরা জাভাস্ক্রিপ্ট এর অ্যাসিনক্রোনাস আচরণের কথা জানি। জাভাস্ক্রিপ্ট কোনো কাজ করতে সময় লাগলে সেখানে অপেক্ষা না করে পরের কোডে চলে যায়। নিচের কোডটি খেয়াল করিঃ 

const function1 = () => {
   setTimeout(function() {
      console.log('1st Function');
   }, 3000)
}
const function2 = () => {
   console.log('2nd Function');
}
function1();
function2();

উপরে যদিও ১ম ফাংশন প্রথমে কল করা হয়েছে, কিন্তু অ্যাসিনক্রোনাস আচরণের জন্য আগে ২য় ফাংশন কল হবে। 

কলব্যাকের ফাংশনের সংজ্ঞা থেকে আমরা জেনেছিলাম এটা আরেকটা ফাংশন এক্সিকিউট হলে পরেই এক্সিকিউট হয়। আর তাই আমরা এখানে এই টেকনিকটা ইউজ করে এরকম পৃথক পৃথকভাবে দুইটা ফাংশন লিখেও কলব্যাকের মাধ্যমে ঠিক যেসময় ফাংশনটাকে কল করা দরকার সেসময়েই করতে পারবো। 

const function1 = (callback) => {
   setTimeout(function() {
      console.log('1st Function');
      callback();
   }, 3000)
}
const function2 = () => {
   console.log('2nd Function');
}
function1(function2);

আমরা এখানে আমাদের মেইন ফাংশন কলের ভিতরে আর্গুমেন্ট হিসেবে আমাদের ফাংশনটাকে পাস করে দিলাম। আর সেটা পরে ঠিক যেখানে দরকার সেখানেই কল করলাম। এটা আমাদের মনের মতো ফলাফল দিবে। মানে একটার পরে আরেকটা। এটা ঠিকই ৩ সেকেন্ড ওয়েট করবে তারপর ফলাফল দিবে। কিন্তু সিরিয়াল মেইন্টেইন করে।

প্রমিজ:

জাভাস্ক্রিপ্ট এর অ্যাসিনক্রোনাস আচরণ সম্পর্কে আমরা জানি। প্রমিসের কাজ হচ্ছে এ ধরনের অ্যাসিনক্রোনাস অপারেশনকে হ্যান্ডল করা। এখন আমরা রিমোট একটা সার্ভারের উপর অপারেশন চালাচ্ছি, কিন্তু ডাটা না আসা পর্যন্ত কিন্তু আমরা বলতে পারি না সে অপারেশন সফল হবে না বিফলে যাবে। আর মূলত এইসব হ্যান্ডল করার জন্যেই প্রমিস কাজ করে। আমাদের বেশিরভাগ ক্ষেত্রেই প্রমিস হ্যান্ডেল করতে হয়। প্রমিসের সিনট্যাক্স অনেকটা এইরকম হয়ে থাকে।

let myPromise = new Promise(function(myResolve, myReject) { 
   // "Producing Code" (May take some time)   
   myResolve(); // when successful   
   myReject();  // when error 
}); 


// "Consuming Code" (Must wait for a fulfilled Promise) 

myPromise.then(
   function(value) { /* code if successful */ },
   function(error) { /* code if some error */ }
);

 

যখনি Producing কোড ফলাফল পেয়ে যায়, এইটা তখন নিচের দুইটা কলব্যাক থেকে একটাকে কল করে।

ফলাফল কল
Success myResolve(result value)
Error myReject(error object)

এখন প্রমিস অবজেক্ট আবার ৩ ধরণের হতে পারে, যেমনঃ 

  • Pending
  • Fulfilled
  • Rejected

Pending অবস্থায় ফলাফল undefined, Fulfilled অবস্থায় ফলাফল একটি ভ্যালু, এবং Rejected অবস্থায় ফলাফল একটি error অবজেক্ট।

নিচের মতো করে প্রমিস ব্যবহার করতে হয়ঃ 

myPromise.then(
  function(value) { /* code if successful */ },
  function(error) { /* code if some error */ }
);

 

async এবং await:

async এবং await প্রমিস লিখতে খুবই সহজ করে। async একটি ফাংশন দিয়ে প্রমিস রিটার্ন করে এবং await একটি ফাংশনকে প্রমিসের জন্য অপেক্ষা করায়। একটা প্রমিস হ্যান্ডেল করার পর এটার ভিতরে কলব্যাক ফাংশন কল করতে হয়। আবার সেই কলব্যাক ফাংশনের ভিতরে প্রমিস থেকে আসা ডাটাগুলো অ্যাক্সেস করতে হয়। এভাবে একটার পর একটার ভিতরে গিয়ে গিয়ে এভাবে আমাদের অপারেশনগুলো চালাতে হয় শুধুমাত্র জাভাস্ক্রিপ্ট এর অ্যাসিনক্রোনাস আচরণের কারণে।

জাভস্ক্রিপ্ট-এ মূলত প্রমিস হ্যান্ডেলের জন্যই async আর await এর পরিচয় করিয়ে দেওয়া হয়েছে। যেখানেই আপনি এরকম অ্যাসিনক্রোনাস কোডকে সিনক্রোনাস আচরণ করাতে চান সেখানেই এগুলো ব্যবহার করতে পারবেন। তবে এখানে কিছু নিয়ম কানুন আছে। আপনাকে async এই কীওয়ার্ডটা ব্যবহার করতে হবে ফাংশনের সাথে। আপনি যে কোডগুলোকে সিনক্রোনাস আচরণ করাতে চাচ্ছেন সেগুলো সবগুলো একটা ফাংশনের ভিতরে ঢুকিয়ে সেই ফাংশনের নামের আগে জাস্ট এভাবে async কীওয়ার্ডটা লাগিয়ে দিবেনঃ

const promiseHandle = async() => {
   const data = await myPromise;
   console.log(data);
}

এখানে দেখুন লক্ষ্য করে আমি ঠিক এর পরের লাইনেই আমাদের প্রমিস থেকে আসা ডাটা প্রিন্ট করে দিয়েছি। হ্যা এখানেই async আর await এর ম্যাজিক। এটা আমাদের অ্যাসিনক্রোনাস কোডকে সিনক্রোনাস আচরণ করতে সাহায্য করে যাতে আমাদের আর কলব্যাকে হেলে পড়তে না হয়। আমরা একদম লাইন বাই লাইন ইন্সট্রাকশন দিয়েই ডাটা উদ্ধার করতে পারবো।

এখন যদি প্রমিস রিজেক্টেড হয় তাহলে? হ্যা তাহলে আমরা  try catch ব্লক দিয়েই আমরা আমাদের এই এরর হ্যান্ডল করতে পারবো। 

 

উদাহরন
  • একটি callback function হল একটি ফাংশন যা একটি argument হিসাবে অন্য ফাংশনে পাস করা হয়, যেটিকে outer function এর অভ্যন্তরে কিছু ধরণের ক্রিয়া সম্পন্ন করার জন্য আহ্বান করা হয়।
function greet(name, callback) {
console.log("HELLO" + " " + name);
callback();
}

function callMe() {
console.log("I am a callback function");
}

greet("VIVASOFT", callMe);

 

উপরের উদাহরণটি একটি synchronous callback, কারণ এটি অবিলম্বে execute করা হয়।

এখানে greet function টি প্রথমে কল হয় যেখানে ২টি parameter পাঠানো হয়। ১টি “VIVASOFT” এবং অপরটি callMe যা callback এর কাজ করে। greet function এর প্রথম statement: console.log(“HELLO” + ” ” + name); এটি execute হবে। তারপর callback function টি execute হবে। সুতরাং নিম্নোক্ত output দেখাবে।

এখন আমরা XMLHttpRequest দিয়ে আমরা একটি POST কল এর উদাহরণ দেখবো

// HELLO VIVASOFT
// I am a callback function

 

  • function greet() {
     console.log("Hello VIVASOFT 2");
    }
     
    function sayHello(name) {
     console.log("Hello" + " " + name);
    }
     
    setTimeout(greet, 2000);
    sayHello("VIVASOFT 1");

উপরের উদাহরণটিতে একটি setTimeout function রয়েছে যেটি কল stack এ চলে যাবে। এর মাঝে sayHello function টি execute হবে। আর setTimeout function এর delayTime শেষ হওয়া মাত্রই queue তে যাবে এবং এর মধ্যে থাকা function টি execute হবে। সুতরাং নিম্নোক্ত output দেখাবে।

// Hello VIVASOFT 1
// Hello VIVASOFT 2

 

  • Callback Hell অথবা Christmas Tree Problem
const add = function (a, b, callback) {
 setTimeout(() => {
   callback(a + b);
 }, 2000);
};

add(1, 2, (sum1) => {
 add(3, sum1, (sum2) => {
   add(4, sum2, (sum3) => {
     add(5, sum3, (sum4) => {
       add(6, sum4, (sum5) => {
         add(7, sum5, (sum6) => {
           add(8, sum6, (sum7) => {
             console.log(`Sum of first 4 natural
             numbers using callback is ${sum7}`);
           });
         });
       });
     });
   });
 });
});

উপরের উদাহরণটি উপলব্ধি করি। ধরে নেই add function টি একটি asynchronous function যা execute হইতে ২০০০ মিলি সেকেন্ড সময় নেয়। এটি ২ টি নাম্বার এবং ১ টি callback function parameter হিসেবে নেয়। প্রথমে add(1, 2, … লাইনটি execute হবে এবং যোগফল ২০০০ মিলি সেকেন্ড সময় পর পাওয়া যাবে। এরপর add(3, sum1, … লাইনটি execute হবে এবং যোগফল ২০০০ মিলি সেকেন্ড সময় পর পাওয়া যাবে যেখানে sum1 = ১ + ২ = ৩ । এভাবে বাকী callback গুলোও কল হবে । সর্বশেষে ১৪০০০ মিলি সেকেন্ড সময় পর নিম্নোক্ত output দেখাবে।

//Sum of first 4 natural numbers using callback is 36

এটি Callback Hell অথবা Christmas Tree Problem নামে পরিচিত। এই ধরনের callback এ error debug করা কঠিন। এর পরের উদাহরণে আমরা Callback Hell অথবা Christmas Tree সমস্যার সমাধান দেখবো।

 

  • উপরোক্ত Callback Hell সমস্যার সমাধান আমরা Promise ব্যবহার করে করব। প্রথমে addPromise নামের একটি function লিখি যেটি Promise return করে। যদি কোন error না হয় তাহলে পরবর্তী then কল হবে।
const addPromise = function (a, b) {
 return new Promise((resolve, reject) => {
   setTimeout(() => {
     resolve(a + b);
   }, 2000);
 });
};
 
addPromise(1, 2)
 .then((sum1) => {
   return addPromise(3, sum1);
 })
 .then((sum2) => {
   return addPromise(4, sum2);
 })
 .then((sum3) => {
   return addPromise(5, sum3);
 })
 .then((sum4) => {
   return addPromise(6, sum4);
 })
 .then((sum5) => {
   return addPromise(7, sum5);
 })
 .then((sum6) => {
   return addPromise(8, sum6);
 })
 .then((sum7) => {
   console.log(
     `Sum of first 8 natural numbers using
      promise and then() is ${sum7}`
   );
 })
 .catch((error) => {
   console.error(error.message);
 });

এভাবে সর্বশেষে ১৪০০০ মিলি সেকেন্ড সময় পর নিম্নোক্ত output দেখাবে।

// Sum of first 8 natural numbers using promise and then() is 36

বিঃ দ্রঃ যদি সম্পূর্ণ execution চলাকালে কোন error হয় তাহলে catch((error) => … এই block এর মধ্যে প্রবেশ করবে এবং এর মধ্যে থাকা statement সমূহ execute করবে। অনেকগুলো async task এ Promise ব্যবহার করার মাধ্যমে আমরা বুঝতে পারলাম এটি কোড readability বাড়ায় এবং সেই সাথে error debugging এর কাজটুকু সহজ করে দেয়।

 

  • Callback Hell অথবা Christmas Tree সমস্যার সমাধান আমরা async / await ব্যবহার করার মাধ্যমেও করতে পারি। নিচের কোডটুকু লক্ষ্য করি।
const addPromise = function (a, b) {
 return new Promise((resolve, reject) => {
   setTimeout(() => {
     resolve(a + b);
   }, 2000);
 });
};
 
async function add1To8() {
 try {
   const sum1 = await addPromise(1, 2);
   const sum2 = await addPromise(3, sum1);
   const sum3 = await addPromise(4, sum2);
   const sum4 = await addPromise(5, sum3);
   const sum5 = await addPromise(6, sum4);
   const sum6 = await addPromise(7, sum5);
   const sum7 = await addPromise(8, sum6);
   console.log("Sum of first 8 natural numbers using async / await is ", sum7);
 } catch (error) {
   console.error(error.message)
 }
}
 
add1To8();

add1To8() function টি একটি async function যার ভিতরে অনেকগুলো await কল করা হয়েছে। ১ম await এর execution শেষ হওয়ার পর ২য় await এর execution শুরু হবে। এভাবে ৩য়, ৪র্থ, . . . ৭ম পর্যন্ত চলবে। এভাবে সর্বশেষে ১৪০০০ মিলি সেকেন্ড সময় পর নিম্নোক্ত output দেখাবে।

// Sum of first 8 natural numbers using async / await is 36

 

বিঃ দ্রঃ যদি সম্পূর্ণ execution চলাকালে কোন error হয় তাহলে অবশিষ্ট await গুলো execute না করে সরাসরি catch((error) => … এই block এর মধ্যে প্রবেশ করবে এবং এর মধ্যে থাকা statement সমূহ execute করবে। অনেকগুলো async / await ব্যবহার করার মাধ্যমে আমরা বুঝতে পারলাম এটি কোড readability বাড়ায় এবং সেই সাথে error debugging এর কাজটুকু আরও সহজ করে দেয়।

এসো নিজে করি
  • একটি function লিখ যেটি একটা সংখ্যা ইনপুট হিসেবে নিবে এবং একে অন্য একটি function এর ভেতর callback আকারে পাঠিয়ে কল করবে এবং নিম্নোক্ত ফলাফল দেখাবে।
    1
    2
    3

     

  • একটি callback hell এর উদাহরণ লিখ।
  • কি কি উপায়ে callback hell সমস্যার সমাধান করা যায় উল্লেখ করুন।
  • নিম্নোক্ত কোডটিকে কিভাবে optimize করা যেতে পারে?
loadScript("/article/promise-chaining/one.js")
 .then(function (script) {
   return loadScript("/article/promise-chaining/two.js");
 })
 .then(function (script) {
   return loadScript("/article/promise-chaining/three.js");
 })
 .then(function (script) {
   one();
   two();
   three();
 });

 

  • নিম্নোক্ত কোডটিকে async/await ব্যবহার করে কিভাবে optimize করা যেতে পারে?
function sleep(delay) {
 return new Promise((resolve) => setTimeout(resolve, delay));
}
 
function sumAsync(x, y) {
 return new Promise((resolve, reject) => {
   sleep(3000).then(() => {
     resolve(x + y);
   });
 });
}
 
sumAsync(5, 7).then((result) => {
 console.log("The result of the addition is:", result);
});