بررسی پرامیس‌ها (Promise) در جاوااسکریپت

یکی از پراستفاده‌ترین ویژگی‌های جاوااسکریپت که از ES6 به این زبان اضافه شده، پرامیس‌ها هستن که توی این قسمت می‌خوایم مفصل اونها رو بررسی کنیم

سلام دوستان. ‌Promise که برای مدیریت کردن عملیات ناهمگام استفاده میشه، یکی از کاربردی‌ترین ویژگی‌های جاوااسکریپت هست که از ES6 به این زبان اضافه شده. توی این قسمت می‌خوایم این موضوع کاربردی رو بررسی کنیم.

توی این قسمت یاد می‌گیریم که:

  • مشکل کجاست
  • پرامیس چیه
  • resolve و reject چی هستن
  • اگه عملیات موفقیت‌آمیز بود
  • اگه عملیات با خطا مواجه شد
  • پاس دادن اطلاعات به Handler ها
  • Handler های پشت سر هم
  • وضعیت‌های پرامیس
  • متد finally
  • مثال دنیای واقعی


مشکل کجاست؟ ?

ابتدا با مفهومی به اسم عملیات ناهمگام آشنا بشیم:

عملیات ناهمگام به عملیاتی گفته میشه جدا از روند اصلی برنامه پردازش میشه

توی جاوااسکریپت، تابع setTimeout یکی از معروف‌ترین مثال‌ها برای درک مفهوم عملیات ناهمگام هست. با این تابع می‌تونیم قطعه کدی رو بعد از یک تأخیر معین اجرا کنیم. کد زیر رو در نظر بگیرین:

setTimeout(function () {
  alert(1); // 1
}, 2000);

alert(2); // 2

ورودی اول setTimeout یک تابع هست و شامل عملیاتی میشه که می‌خوایم اون رو با تأخیر انجام بدیم. ورودی دوم مدت زمان تأخیر به میلی‌ثانیه هست. اینجا 2000 یعنی 2 ثانیه.
بعد از اجرای کد می‌بینیم که ابتدا alert خط ۵ نمایش داده میشه و بعد از ۲ ثانیه alert خط ۲. این یعنی یک عملیات ناهمگام داریم. شاید انتظار داشتیم که جاوااسکریپت خط به خط کدهای ما رو بررسی کنه و خروجی‌ها رو نمایش بده. اما وقتی که جاوااسکریپت به قسمتی از کد رسید که شامل یک عملیات ناهمگام بود، منتظر نتیجه این قسمت نموند و به ادامه بررسی خط‌های بعدی پرداخت.

توی جاوااسکریپت، یک سری عملیات مثل اتصال به یک منبع خارجی با Ajax و WebSocket و یا خوندن و نوشتن اطلاعات توی دیتابیس و هارددیسک، جدا از روند اصلی برنامه اجرا میشن. این یعنی عملیات ناهمگام.

چطوری می‌تونیم بعد از به پایان رسیدن یک عملیات ناهمگام کاری رو انجام بدیم؟ مثلاً توی کد بالا ابتدا صبر کنیم کار تابعی که به setTimeout پاس دادیم تموم بشه و بعد alert با مقدار 2 رو بگیریم؟ باید راهی باشه که بتونیم عملیات ناهمگام رو مدیریت کنیم. با پرامیس‌ها این کار رو می‌تونیم انجام بدیم ?


پرامیس چیه؟

پرامیس‌ها برای مدیریت کردن عملیات ناهمگام استفاده میشن. نحوه نوشتن یک پرامیس خیلی راحته. ابتدا با ساختار اون بشیم و بعد مثال بالا رو با پرامیس خواهیم نوشت:

new Promise(function (resolve, reject) {
  // do something ...
});

همونطور که می‌بینیم، یک پرامیس با ساختن یک نمونه از کلاس ‌Promise با استفاده از کلمه‌کلیدی new ساخته میشه. کلاس پرامیس فقط یک آرگومان می‌گیره و اون هم یک تابع هست. این تابع که توی اون باید عملیات ناهمگامِمون رو بنویسیم، بلافاصله وقتی یک پرامیس ساخته شد اجرا میشه.

حالا بیاین مشکلی که توی مثالمون داشتیم رو با پرامیس‌ها حل کنیم تا با ساختار اون بیشتر آشنا بشیم.

const welcome = new Promise(function (resolve, reject) {
  setTimeout(() => {
    alert(1);
     resolve();
  }, 2000);
});

welcome.then(function () {
  alert(2);
});

با اجرای این کد ابتدا با یک تاخیر ۲ ثانیه‌ای alert با مقدار 1 می‌گیریم و بعد 2. اینطوری تونستیم عملیات ناهمگام رو مدیریت و به قول معروف هندل کنیم ?

حالا چیزهایی که نوشتیم رو بررسی کنیم.


resolve و reject چی هستن؟

همونطور که می‌بینیم تابعی که به پرامیس پاس دادیم دو تا پارامتر می‌گیره که اولی resolve و دومی reject هست:

new Promise(function (resolve, reject) {
  // ...

هر دو پارامتر باید به شکل یک تابع توی پرامیس صدا زده بشن. یعنی:

new Promise(function (resolve, reject) {
  resolve();
  // or
  reject();
});

ما توی پرامیس باید به جاوااسکریپت بگیم که کارِ ما به پایان رسید. چون جاوااسکریپت خودش متوجه به پایان رسیدن این عملیات نمیشه. وقتی عملیات ما به پایان رسید، اگر خروجی کار موفقیت‌آمیز بود، باید resolve رو صدا بزنیم و اگر خطا داشت reject.


اگه عملیات موفقیت‌آمیز بود

توی پرامیسی که نوشتیم توی خط ۴ resolve رو صدا زدیم به این معنی که عملیات ما با موفقیت به پایان رسیده:

const welcome = new Promise(function (resolve, reject) {
  setTimeout(() => {
    alert(1);
     resolve();
  }, 2000);
});

چرا باید به جاوااسکریپت بگیم که عملیات ما به پایان رسید؟ برای اینکه بتونیم بعد از به پایان رسیدن عملیات ناهمگام، کار مد نظرمون رو انجام بدیم. توی مثالی که داشتیم، کار مد نظرمون رو به این صورت انجام دادیم:

welcome.then(function () {
  alert(2);
});

همونطور که می‌بینیم از متغیر welcome که مقدار اون یک پرامیس هست، یک متد به اسم then رو صدا زدیم و به اون تابعی رو پاس دادیم. این تابع بلافاصله زمانی اجرا میشه که ما توی پرامیس resolve رو صدا زده باشیم! پس کاری که می‌خوایم بعد از به پایان رسیدن عملیات ناهمگام اجرا بشه رو با then انجام می‌دیم.


اگه عملیات با خطا مواجه شد

اگه عملیات ناهمگام با خطا مواجه شد، باید توی پرامیس reject رو صدا بزنیم:

const welcome = new Promise(function (resolve, reject) {
  const status = 'failed';

  setTimeout(() => {
    if (status === 'failed') {
       reject();
    }
  }, 2000);
});

این کار به جاوااسکریپت میگه که عملیات ناهمگاممون موفقیت‌آمیز نبود. اگه بخوایم توی برنامه، reject شدن یک پرامیس رو مدیریت کنیم از متد catch که مشابه then هست استفاده می‌کنیم:

const welcome = new Promise(function (resolve, reject) {
  const status = 'failed';

  setTimeout(() => {
    if (status === 'failed') {
       reject();
    }
  }, 2000);
});

welcome.catch(function () {
  alert(&quotPromise failed!&quot);
});

همونطور که می‌بینیم، توی catch یک تابع تعریف کردیم. این تابع بلافاصله بعد از اینکه پرامیس reject شد اجرا میشه.

به متدهای then و catch می‌گیم Handler به‌معنی کنترل‌کننده نتیجه عملیات ناهمگام.

ما برای مدیریت کردن خطای پرامیس می‌تونیم بجای متد catch از آرگومان دوم متد then کمک بگیریم:

const welcome = new Promise(function (resolve, reject) {
  reject();
});

welcome.then(
  function () { alert(&quotSucceeded&quot) },
   function () { alert(&quotFailed&quot) }
);

تابعی که به آرگومان دوم پاس دادیم فقط زمانی اجرا میشه که reject صدا زده بشه. در واقع متد catch مشابه کد زیر هست:

myPromise.then(null, function () {

});

پاس دادن اطلاعات به Handler ها

هر چیزی که به توابع resolve و reject پاس بدیم، توی متدهای then و catch قابل دسترس هست:

const welcome = new Promise(function (resolve, reject) {
  resolve(&quotHello!&quot);
});

welcome.then(function (response) {
  alert(response); // Hello!
});


Handler های پشت سر هم

یک پرامیس وقتی که resolve میشه می‌تونه چندین Handler داشته باشه. به این معنی که بتونیم کارهای مختلفی رو انجام بدیم:

const welcome = new Promise(function (resolve, reject) {
  resolve(1);
});

welcome.then(res => alert(res + 1)); // 2
welcome.then(res => alert(res + 2)); // 3
welcome.then(res => alert(res + 3)); // 4

Handler های پشت سر هم می‌تونن به همدیگه بچسبن:

const welcome = new Promise(function (resolve, reject) {
  resolve(1);
});

welcome.then().then().then();

توی چنین شرایطی، خروجی هر then به then بعدی پاس داده میشه:

const welcome = new Promise(function (resolve, reject) {
  resolve(&quotHello&quot);
});

welcome
  .then(res1 => res1.split(&quot&quot))      // [&quotH&quot, &quote&quot, &quotl&quot, &quotl&quot, &quoto&quot]
  .then(res2 => res2.reverse())      // [&quoto&quot, &quotl&quot, &quotl&quot, &quote&quot, &quotH&quot]
  .then(res3 => res3.join(&quot&quot))       // olleH
  .then(res4 => alert(res4));        // olleH


وضعیت‌های پرامیس

دیدیم که نتیجه پرامیس یا موفقیت‌آمیز هست یا رد شده. در واقع یک پرامیس همیشه سه حالت داره: Pending - Rejected - Fulfilled


۱. حالت Pending

این حالت یعنی حالت انتظار. پرامیس در ابتدا و وقتی که ساخته میشه، در این حالت قرار داره:

const asyncOpr = new Promise(function(resolve, reject) {
  // no resolve or reject
})

console.log(asyncOpr);

این حالت تا زمانی که resolve یا reject صدا زده نشه باقی می‌مونه. کد را اجرا کنید و کنسول رو ببینین.

۲. حالت Fulfilled

حالت پرامیس وقتی resolve رو صدا بزنیم در حالت Fulfilled یعنی برآورده شده و موفقیت‌آمیز بودن قرار می‌گیره:

new Promise(function (resolve, reject) {
  setTimeout(function() {
     resolve();
  }, 1000);
});

توی این کد، پرامیس بعد از ۱ ثانیه توی وضعیت Fulfilled قرار می‌گیره چون resolve رو صدا زدیم.


۳. حالت Rejected

حالت پرامیس وقتی reject رو صدا بزنیم در حالت Rejected قرار می‌گیره. یعنی عملیات ناهمگام موفقیت‌آمیز نبود:

new Promise(function (resolve, reject) {
  setTimeout(function() {
     reject();
  }, 1000);
});

توی این کد، پرامیس بعد از ۱ ثانیه توی وضعیت Rejected قرار می‌گیره چون reject رو صدا زدیم.



نکته‌ای که باید در نظر داشته باشیم اینه که پرامیس فقط می‌تونه توی یک کدوم از حالت‌های Fulfilled و یا Rejected قرار بگیره:

new Promise(function (resolve, reject) {
  resolve();
  reject();
});

توی این کد، با توجه به اینکه resolve زودتر صدا زده شده، reject کلاً نادیده گرفته میشه. پس catch هم اجرا نخواهد شد:

const welcome = new Promise(function (resolve, reject) {
  resolve();
  reject(); // ignored
});

welcome.then(() => {
  alert(&quotSuccess!&quot); //Success!
});

welcome.catch(() => {
  alert(&quotFailed!&quot);
});

اما اگه reject زودتر صدا زده بشه، then نادیده گرفته میشه.



متد finally

اگه کاری داریم که می‌خوایم در هر دو صورت (هم resolve شدن و هم reject شدن) انجام بدیم، از یک Handler دیگه به اسم finally استفاده می‌کنیم:

const input = prompt(&quotEnter 1&quot);

const welcome = new Promise((resolve, reject) => {
  if (input == 1) {
    resolve(&quotSucceeded!&quot);
  } else {
    reject(&quotFailed!&quot);
  }
});

welcome.then(alert);
welcome.catch(alert);

welcome.finally(() => {
  clearCache();
});

function clearCache() {
  alert(&quotProgram cache cleared!&quot);
}

همونطور که می‌بینیم کدی که توی خط ۱۵ نوشتیم تحت هر شرایطی اجرا میشه. تابعی که به finally پاس دادیم هیچ آرگومانی قبول نمی‌کنه. چون نتیجه عملیات براش مهم نیست.

بدون استفاده از finally کد بالا رو باید اینطوری می‌نوشتیم:

welcome.then(
  function (success) { 
    clearCache();
  },
  function(error) {
     clearCache();
  }
);

اما این راه مناسب نیست چون کد تکراری داریم می‌نویسیم.


مثال دنیای واقعی

توی کد زیر تابعی داریم که با اون می‌تونیم اسکریپت‌های مختلفی (مثل جی‌کوئری، Axios و ...) رو از طریق URL به صفحه اضافه کنیم:

function loadScript(path) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = path;
    script. = () => resolve(path); 
    script. = () => reject(new Error('Whoops'));
	 
    document.head.append(script);
  });
}

توی این تابع یک پرامیس نوشتیم که توی اون اسکریپت رو به صفحه اضافه می‌کنیم و به محض اینکه اسکریپت دانلود بشه، پرامیس resolve میشه. توی تابع loadScript یک پرامیس داره return میشه. پس خروجی این تابع یک پرامیس هست.

حالا می‌خوایم کتابخونه Axios که محبوب‌ترین ابزار برای ارسال درخواست‌های Ajax هست رو به صفحه اضافه و از اون استفاده کنیم:

const loadAxios = loadScript(&quothttps://unpkg.com/axios/dist/axios.min.js&quot);

الان مقدار متغیر loadAxios یک پرامیس هست و به محض اینکه اسکریپت مد نظرمون لود بشه می‌خوایم از اون استفاده کنیم. از متدهای then یا catch به صورت زیر استفاده می‌کنیم:

loadAxios.then(() => {
  // Axios is loaded  
  axios.get(path);
});

همونطور که می‌دونیم کتابخونه Axios هم مبتنی بر پرامیس‌ها هست. یعنی نتیجهٔ درخواست‌هایی که با اون می‌فرستیم رو می‌تونیم به شکل پرامیس‌ها مدیریت کنیم. توی این مثال می‌خوایم به یک آدرس خارجی یک درخواست Ajax بزنیم و نتیجه رو به کاربر نشون بدیم:

const loadAxios = loadScript(&quothttps://unpkg.com/axios/dist/axios.min.js&quot);

loadAxios.then(() => {
  axios.get(&quothttps://randomuser.me/api/?inc=name&results=3&quot)
    .then(response => {
      if (response.status === 200) {
        const users = response.data.results;

        for (const {name} of users) {
          alert(`Welcome ${name.title} ${name.first} ${name.last}! ?`);
        }
      }
    });
});

function loadScript(path) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = path;
    script. = () => resolve(path); 
    script. = () => reject(new Error('Whoops'));
	 
    document.head.append(script);
  });
}

با اجرای کد می‌بینیم که ابتدا Axios به صفحه اضافه میشه و بعد با استفاده از اون، درخواستی به آدرس مد نظرمون می‌زنیم تا اطلاعات رو به کاربر نشون بدیم. توی این برنامه ۲ بار از پرامیس‌ها استفاده کردیم ??




خب دوستان این قسمت هم به پایان رسید و امیدوارم استفاده کرده باشید. برای ادامه آموزش به قسمت‌های بعدی برین. روزتون خوش ? ?





Resources :

ditty