مقدمه

Promise یکی از مهم‌ترین ابزارهای مدیریت عملیات غیرهمزمان در جاوااسکریپت است که با ساده‌تر کردن مدیریت توالی عملیات، مدیریت خطا و خوانایی کدها را افزایش می‌دهد. Promise جایگزین callbackهای تو در تو شده و کد شما را خواناتر و حرفه‌ای‌تر می‌کند. Promise یک شیء است که نتیجه‌ی یک عملیات غیرهمزمان (مانند درخواست AJAX یا تایمر) را نمایندگی می‌کند و می‌تواند سه حالت داشته باشد: pending (در حال انتظار)، fulfilled (موفق) و rejected (شکست‌خورده). در ادامه به جزئیات مربوط به شیء Promise خواهیم پرداخت.

ساختار یک Promise

در درس قبل دیدیم که callbackها می‌توانند باعث پیچیدگی و کاهش خوانایی کد شوند. Promise با ارائه یک ساختار استاندارد، این مشکل را حل می‌کند و به شما اجازه می‌دهد عملیات غیرهمزمان را به صورت زنجیره‌ای و قابل مدیریت بنویسید. Promise در جاوااسکریپت یک شیء است که نمایانگر نتیجه‌ی نهایی یک عملیات غیرهمزمان است. در واقع، Promise همانطور که از نامش پیداست، قول می‌دهد که در آینده یا نتیجه‌ی یک عملیات را برگرداند یا دلیل خطا را اعلام کند.

برای ساخت Promise جدید باید تابع سازنده آن را صدا بزنیم و داخل آن تابعی بسازیم که resolve و reject را دیافت کرده و بسته به نتیجه‌ی عملیات، Promise را resolve یا reject کنیم. مثال زیر یک Promise را پس از دو ثانیه با موفقیت resolve می‌کند.

Copy Icon JAVASCRIPT
let p = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve("success!");
  }, 2000);
});

p.then(function(result) {
  console.log(result); // success!
});

در اینجا ابتدا یک شیء Promise ساخته‌ایم و آن را به متغیر p نسبت داده‌ایم. تابعی که به Promise پاس شده، دو آرگومان دارد: یکی resolve که اگر عملیات موفقیت‌آمیز باشد این تابع را صدا می‌زنیم و دیگری reject که در صورت شکست عملیات، فراخوانی می‌شود. داخل این تابع هم از تابع setTimeout استفاده کرده‌ایم تا یک عملیات غیرهمزمان را شبیه‌سازی کنیم. بعد از 2 ثانیه تابعی که در آن resolve("success") صدا زده شده، اجرا می‌شود و به این ترتیب، Promise با موفقیت به پایان می‌رسد. سپس، از متد then برای دریافت نتیجه‌ی موفق Promise استفاده کرده‌ایم. این متد یک تابع می‌گیرد که پارامتر آن همان مقداری است که با resolve داده شد. بعد از 2 ثانیه مقدار "success" به این تابع داده شده و در کنسول چاپ می‌شود.

به طور خلاصه، در اینجا Promise پس از ۲ ثانیه resolve می‌شود و مقدار "success" را به تابع then منتقل می‌کند. اگر عملیات با خطا مواجه شود و reject فراخوانی شود، می‌توانید با استفاده از متد catch خطا را مدیریت کنید:

Copy Icon JAVASCRIPT
let p = new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject("error!");
  }, 2000);
});
p.then(function(result) {
    console.log(result);
}).catch(function(error) {
    console.error(error); //error!
});

در اینجا اگر به جای resolve، تابع reject را صدا بزنید، Promise وارد حالت شکست می‌شود و می‌توانید با متد catch خطا را مدیریت کنید. این ساختار باعث می‌شود مدیریت خطاها در عملیات غیرهمزمان بسیار ساده‌تر و قابل پیش‌بینی‌تر باشد.

چرخه اجرای کد Promise

وقتی یک Promise ساخته می‌شود، ابتدا در وضعیت pending قرار دارد. کد درون تابع Promise که معمولاً یک عملیات غیرهمزمان مانند درخواست شبکه، کار با فایل یا استفاده از setTimeout است، اجرا شده و بسته به موفقیت یا شکست عملیات، Promise در یکی از دو وضعیت fulfilled یا rejected قرار می‌گیرد. در وضعیت fulfilled، تابع resolve صدا زده شده و و مقدار داده شده به آن به اولین تابعی که با then ثبت شده، پاس می‌شود. اگر بیش از یک تابع then تعریف شده باشد، یکی یکی اجرا می‌شوند و مقدار را دریافت می‌کنند. اما در وضعیت rejected، تابع reject صدا زده شده و مقدار داده شده به آن به اولین تابعی که با catch ثبت شده، پاس داده می‌شود.

        +-------------------------+
        |      Promise (pending)  |
        +-------------------------+
              /            \
             /              \
            v                v
    +----------------+  +-----------------+
    |  resolve()     |  |   reject()      |
    +----------------+  +-----------------+
            |                   |
            v                   v
    +----------------+  +-----------------+
    |  fulfilled     |  |   rejected      |
    +----------------+  +-----------------+
            |                   |
            v                   v
    .then() handler       .catch() handler

                    

زنجیره Promise و مدیریت خطا

با Promise می‌توان عملیات غیرهمزمان را به صورت زنجیره‌ای و بدون نیاز به تودرتوسازی نوشت و این از مهمترین مزایایی است که Promise نسبت به توابع Callback دارد. مثال زیر را ببینید.

Copy Icon JAVASCRIPT
new Promise(function(resolve, reject) {
  setTimeout(() => resolve(5), 1000);
})
.then(x => x * 2)
.then(x => x + 1)
.then(console.log) // 11
.catch(err => console.error(err));

این مثال، مقدار اولیه ۵ را دریافت و دو بار پردازش می‌کند. اگر هر مرحله خطا بدهد، catch اجرا می‌شود. این شیوه خوانایی و کنترل خطا را بسیار ساده‌تر کرده است.

Promise.all و اجرای موازی

Promise.all یک متد استاتیک در جاوااسکریپت است که به شما اجازه می‌دهد چندین Promise را همزمان اجرا کنید و زمانی که همهٔ آن‌ها با موفقیت (resolve) به پایان رسیدند، نتیجه تمام آن‌ها را با هم دریافت کنید. اگر همه Promiseها موفق شوند، Promise نهایی با آرایه‌ای از نتایج resolve می‌شود. اگر حتی یکی از Promiseها شکست بخورد (reject شود)، کل Promise نهایی فوراً رد می‌شود و مقدار خطا همان خطای اولین Promise شکست‌خورده خواهد بود.

متد Promise.all دارای ساختار کلی زیر است:

Copy Icon JAVASCRIPT
Promise.all([promise1, promise2, promise3])
  .then(results => {
    // All prmises resolved
    // results is an array of results 
  })
  .catch(error => {
    // One or more promises rejected 
    // error is the value of error for the first rejected promise
  });

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

Copy Icon JAVASCRIPT
let p1 = Promise.resolve(3);
let p2 = Promise.resolve(7);
Promise.all([p1, p2]).then(function(results) {
  console.log(results); // [3, 7]
});

Promise.all منتظر می‌ماند تا هر دو Promise کامل شوند و سپس آرایه‌ای از نتایج را بازمی‌گرداند. اگر هر کدام reject شود، کل Promise شکست می‌خورد.