FA-TOOLS — Header Component
آموزش promise در جاوا اسکریپت

آموزش جامع Promise در جاوا اسکریپت؛ از مفاهیم پایه تا مدیریت پیشرفته عملیات ناهمگام

فهرست مطالب مقاله

  • Promise چیست و چرا به آن نیاز داریم؟
  • حالات سه‌گانه یک Promise
  • نحوه ساخت و تعریف یک Promise جدید
  • مدیریت نتایج با then، catch و finally
  • متدهای ایستا (Promise.all, Promise.race و…)
  • ارتباط Promise با Async/Await
  • مفهوم Microtask Queue و رفتار برنامه در مرورگر
  • سناریوهای واقعی و تکنیک‌های پیشرفته کاربردی
  • جدول مقایسه روش‌های مدیریت عملیات ناهمگام
  • پرسش‌های متداول
  • عیب‌یابی سریع مشکلات رایج
خلاصه سریع نکات کلیدی:

  • پراپیر (Promise) راهکاری مدرن برای مدیریت کدهای ناهمگام (Asynchronous) و جلوگیری از جهنم کال‌بک‌ها (Callback Hell) است.
  • هر Promise همواره در یکی از ۳ حالت Pending (در حال انتظار)، Fulfilled (موفق) یا Rejected (شکست‌خورده) قرار دارد.
  • زنجیره‌سازی (Chaining) با .then() به شما اجازه می‌دهد خروجی یک کد ناهمگام را به ورودی مرحله بعدی تبدیل کنید.
  • متدهای ایستایی مثل Promise.allSettled و Promise.race کنترل دقیقی روی اجرای هم‌زمان چند ریکوئست ارائه می‌دهند.

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

Promise چیست و چرا به آن نیاز داریم؟

Promise چیست و چرا به آن نیاز داریم؟

پاسخ کوتاه: Promise در جاوا اسکریپت شیئی است که نماینده نتیجه نهایی (موفقیت یا شکست) یک عملیات ناهمگام در آینده است. این ابزار به برنامه اجازه می‌دهد تا زمان دریافت پاسخ واقعی، اجرای سایر بخش‌ها را مسدود نکند.

قبل از معرفی Promise در ES6 (سال ۲۰۱۵)، تمام کدهای ناهمگام با استفاده از توابع Callback مدیریت می‌شدند. اگر نیازمند اجرای چند عملیات متوالی بودید، توابع کال‌بک درون یکدیگر توئید می‌شدند و ساختاری پیچیده، غیرقابل خواندن و آسیب‌پذیر به نام «Callback Hell» یا «Pyramid of Doom» را ایجاد می‌کردند.

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

حالات سه‌گانه یک Promise

حالات سه‌گانه یک Promise

یک Promise از لحظه ایجاد تا زمان تکمیل، دقیقاً در یکی از سه حالت زیر قرار می‌گیرد:

۱. Pending (در حال انتظار):
حالت اولیه Promise است. عملیات ناهمگام شروع شده اما هنوز تمام یا متوقف نشده است.
۲. Fulfilled (انجام‌شده/موفق):
عملیات با موفقیت به پایان رسیده و مقدار خروجی (Value) مشخص شده است.
۳. Rejected (ردشده/شکست‌خورده):
خطایی در حین اجرای عملیات رخ داده و دلیل شکست (Reason) ثبت شده است.

نکته فنی و حیاتی این است که Promise تغییرناپذیر (Immutable) است. وقتی یک Promise به حالت Fulfilled یا Rejected برسد (که اصطلاحاً به آن Settled می‌گویند)، حالت آن دیگر تحت هیچ شرایطی تغییر نخواهد کرد و مقادیر مربوط به آن تثبیت می‌شوند.

نحوه ساخت و تعریف یک Promise جدید

نحوه ساخت و تعریف یک Promise جدید

برای ساخت یک Promise از سازنده new Promise() استفاده می‌شود. این سازنده یک تابع به نام Executor می‌گیرد که خود دارای دو پارامتر ورودی است: resolve و reject.

  1. اگر عملیات موفقیت‌آمیز بود، تابع resolve(value) را فراخوانی می‌کنید.
  2. اگر خطایی پیش آمد، تابع reject(error) را فراخوانی می‌کنید.

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

const fetchUserData = (userId) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: "Ali", role: "Developer" });
      } else {
        reject(new Error("شناسه کاربر معتبر نیست!"));
      }
    }, 1500);
  });
};

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

مدیریت نتایج با then، catch و finally

مدیریت نتایج با then، catch و finally

پاسخ کوتاه: برای دسترسی به نتیجه یک Promise از متد .then() برای موفقیت، .catch() برای مدیریت خطا و .finally() برای کدهایی که در هر شرایطی باید اجرا شوند (مانند بستن Loader) استفاده می‌شود.

ساختار مصرف Promise ساخته‌شده در بخش قبل به شکل زیر است:

fetchUserData(5)
  .then((user) => {
    console.log("اطلاعات کاربر دریافت شد:", user.name);
    return user.id;
  })
  .then((id) => {
    console.log("شناسه پردازش‌شده:", id);
  })
  .catch((error) => {
    console.error("خطا در دریافت اطلاعات:", error.message);
  })
  .finally(() => {
    console.log("پایان عملیات دریافت داده.");
  });

مفهوم زنجیره‌سازی (Promise Chaining)

هر بار که متد .then() اجرا می‌شود، خود یک Promise جدید برمی‌گرداند. این ویژگی امکان زنجیره‌سازی کدهای ناهمگام را فراهم می‌کند. هر مقداری که درون یک .then() ریترن کنید، به‌صورت خودکار به‌صورت Wrapشده درون یک Promise حل‌شده به .then() بعدی منتقل می‌شود.

اگر در هر نقطه‌ای از زنجیره خطایی رخ دهد، مرورگر اجرا را متوقف کرده و مستقیماً به نزدیک‌ترین بلوک .catch() می‌پرد. این ویژگی از انتشار خطاهای مدیریت‌نشده در برنامه جلوگیری می‌کند.

متدهای ایستا (Static Methods) در Promise

متدهای ایستا (Static Methods) در Promise

برای مدیریت هم‌زمان چندین Promise، کلاس Promise چند متد کاربردی ارائه می‌دهد که فرایند توسعه نرم‌افزار را بسیار ساده‌تر می‌کنند:

  • Promise.all([promises]): تمام Promiseها را به‌صورت هم‌زمان اجرا می‌کند. اگر همه موفق شوند، آرایه‌ای از نتایج را برمی‌گرداند. اگر حتی یکی شکست بخورد، کل عملیات بلافاصله با خطا همراه می‌شود (Short-circuit).
  • Promise.allSettled([promises]): منتظر می‌ماند تا تکلیف تمام Promiseها (چه موفق چه با خطا) مشخص شود و آرایه‌ای از اشیا شامل وضعیت و مقدار/دلیل شکست ارائه می‌دهد.
  • Promise.race([promises]): اولین Promise که تعیین تکلیف شود (چه موفق شود و چه شکست بخورد)، نتیجه آن را برمی‌گرداند.
  • Promise.any([promises]): اولین Promise که با موفقیت انجام شود را برمی‌گرداند. اگر همه شکست بخورند، خطای AggregateError ایجاد می‌کند.
  • Promise.resolve(value) / Promise.reject(reason): سریعاً یک Promise حل‌شده یا ردشده با مقدار داده‌شده ایجاد می‌کنند.
متد ایستا رفتار در صورت شکست یک Promise
Promise.all بلافاصله کل فرایند شکست می‌خورد و بقیه نتایج نادیده گرفته می‌شوند.
Promise.allSettled تاثیر منفی ندارد؛ وضعیت شکست برای آن Promise خاص ثبت شده و بقیه اجرا می‌شوند.
Promise.race اگر اولین Promise که تمام می‌شود با خطا باشد، کل race شکست می‌خورد.
Promise.any خطا نادیده گرفته می‌شود و منتظر اولین Promise موفق بعدی می‌ماند.

ارتباط Promise با Async/Await

پاسخ کوتاه: کلمات کلیدی Async/Await یک پوسته روی Promiseها (Syntactic Sugar) هستند که به برنامه نویس اجازه می‌دهند کدهای ناهمگام را با ساختاری شبیه به کدهای همگام و با استفاده از بلوک‌های سنتی try...catch بنویسد.

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

async function getUser() {
  try {
    const user = await fetchUserData(10);
    console.log("اطلاعات دریافت شد:", user);
  } catch (error) {
    console.error("مدیریت خطا با try/catch:", error.message);
  } finally {
    console.log("عملیات به پایان رسید.");
  }
}

استفاده از این ساختار خوانایی کد را به‌ویژه در برنامه‌های پیچیده به میزان چشمگیری افزایش داده و فرایند اشکال‌زدایی (Debugging) را راحت‌تر می‌کند.

مفهوم Microtask Queue و رفتار برنامه در مرورگر

برای درک عمیق رفتار Promise در موتور جاوا اسکریپت (V8)، باید بدانید کدهای مرتبط با Promise چگونه اولویت‌بندی می‌شوند. جاوا اسکریپت تک‌نخی (Single-threaded) است و برای اجرای کدهای ناهمگام از Event Loop استفاده می‌کند.

در حلقه رویدادها، دو صف مجزا برای وظایف وجود دارد:

  • Macrotask Queue: شامل مواردی مثل setTimeout, setInterval و رویدادهای کلیک است.
  • Microtask Queue: شامل کال‌بک‌های Promise (مانند .then, .catch) و queueMicrotask است.

اولویت اجرای صف Microtask به‌شدت بالاتر از Macrotask است. وقتی یک Stack تخلیه می‌شود، موتور جاوا اسکریپت ابتدا تمام کد موجود در Microtask Queue را اجرا می‌کند و سپس به سراغ Macrotask می‌رود. به کد زیر دقت کنید:

console.log("1. شروع");

setTimeout(() => {
  console.log("2. تایمر مکروتاسک");
}, 0);

Promise.resolve().then(() => {
  console.log("3. پرامیس میکروتاسک");
});

console.log("4. پایان");

// خروجی به ترتیب: 1 -> 4 -> 3 -> 2

فهم این اولویت زمانی که مشغول تغییرات پیچیده در UI یا سناریوهایی مانند مفاهیم تعامل با DOM هستید، از بروز باگ‌های عجیب و رفتارهای غیرمنتظره در مرورگر جلوگیری می‌کند.

سناریوهای واقعی و تکنیک‌های پیشرفته کاربردی

۱. پیاده‌سازی Timeout برای درخواست‌های شبکه

گاهی یک API پاسخ نمی‌دهد و نباید کاربر را بی‌نهایت معطل نگه داشت. با ترکیب Promise.race و یک تایمر، می‌توان محدودیت زمانی برای درخواست در نظر گرفت:

const timeout = (ms) => {
  return new Promise((_, reject) => 
    setTimeout(() => reject(new Error("زمان درخواست به پایان رسید!")), ms)
  );
};

// اجرای هم‌زمان درخواست و تایمر
Promise.race([
  fetch("https://api.example.com/data"),
  timeout(3000)
])
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error(err.message));

۲. لغو درخواست‌های ارسال‌شده با AbortController

اگرچه Promise استاندارد به‌طور مستقیم متد لغو (Cancel) ندارد، اما هنگام کار با APIهایی مانند Fetch می‌توانید از AbortController استفاده کنید تا از اتلاف منابع شبکه جلوگیری کنید. این کار در بهینه‌سازی کدهای بخش فرانت‌اند تاثیر مستقیم دارد و بار پردازشی مرورگر را کم می‌کند. این بهینه‌سازی‌ها بر کارایی کلی وب‌سایت اثرگذار بوده و می‌تواند نرخ ماندگاری کاربران در وب‌سایت را به دلیل تجربه کاربری روان‌تر بهبود ببخشد.

جدول مقایسه روش‌های مدیریت عملیات ناهمگام

در جدول زیر سه شیوه اصی مدیریت عملیات ناهمگام در جاوا اسکریپت با یکدیگر مقایسه شده‌اند:

ویژگی / الگوی کدنویسی Callbacks Promises Async / Await
خوانایی کد ضعیف (پیچیده) متوسط عالی (تمیز و همگام‌نما)
مدیریت خطا دست‌ساز و سخت استفاده از .catch() استفاده از try/catch
اجرای هم‌زمان چند کار بسیار دشوار ساده با Promise.all ساده با ترکیب Promise.all

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

پرسش‌های متداول

تفاوت اصلی بین Promise.all و Promise.allSettled چیست؟
متد Promise.all به محض اینکه یکی از درخواست‌ها با خطا مواجه شود، کلا شکست می‌خورد؛ اما Promise.allSettled منتظر می‌ماند تا تمام درخواست‌ها تمام شوند و وضعیت هرکدام (موفق یا ناموفق) را در قالب یک آرایه بازمی‌گرداند.

آیا Promiseها کاملاً ناهمگام هستند؟
بدنه تابع (Executor) که به سازنده Promise داده می‌شود به‌صورت همگام (Synchronous) اجرا می‌شود، اما متدهای .then() و .catch() همیشه به‌صورت ناهمگام و در صف Microtask قرار می‌گیرند.

اگر در یک Promise متد resolve یا reject را فراخوانی نکنیم چه می‌شود؟
آن Promise برای همیشه در حالت Pending می‌ماند و کدهای وابسته درون .then() یا await هرگز اجرا نخواهند شد که این موضوع باعث ایجاد Memory Leak می‌شود.

آیا امکان لغو (Cancel) یک Promise در جاوا اسکریپت وجود دارد؟
خیر، شیء Promise بومی در جاوا اسکریپت متدی برای Cancelling ندارد. برای لغو عملیات شبکه باید از ابزارهایی مانند AbortController استفاده کنید.

عیب‌یابی سریع مشکلات رایج

۱. خطای Uncaught (in promise) Error

علت: یک Promise رد (Reject) شده است اما هیچ بلوک .catch() یا try...catch برای دریافت و مدیریت این خطا وجود ندارد.

راه حل: همواره در انتهای زنجیره Promise از .catch() استفاده کنید یا هنگام استفاده از await، کد را درون بلوک try...catch بگذارید.

۲. فراموش کردن return در زنجیره then

علت: در یک .then() عملیات ناهمگام دیگری شروع شده اما مقدار یا پرامیس آن return نشده است. این کار باعث می‌شود .then() بعدی منتظر پایان عملیات نماند و مقدار undefined دریافت کند.

راه حل: مطمئن شوید که تمام توابع ناهمگام درون .then() مقدار خود یا Promise حاصل را return می‌کنند.

۳. کندی برنامه به دلیل اجرای متوالی درخواست‌های مستقل با await

علت: نوشتن چند await پشت سر هم برای درخواست‌هایی که به یکدیگر وابسته نیستند، باعث می‌شود درخواست دوم تا پایان درخواست اول معطل بماند.

راه حل: درخواست‌های مستقل را در قالب یک آرایه به Promise.all() بدهید تا به‌صورت هم‌زمان (Parallel) اجرا شوند.

سوالات متداول

Promise در جاوا اسکریپت چیست و چه کاربردی دارد؟

پرامیس شیئی است که نماینده نتیجه نهایی یک عملیات ناهمگام در آینده است. این ابزار مانع مسدود شدن اجرای برنامه شده و کدهای ناهمگام را تمیز و قابل مدیریت می‌سازد.

حالات سه‌گانه یک Promise کدامند؟

هر پرامیس همواره در یکی از سه حالت Pending (در حال انتظار)، Fulfilled (تکمیل موفقیت‌آمیز) یا Rejected (شکست‌خورده همراه با خطا) قرار می‌گیرد.

مشکل Callback Hell چیست و چگونه با Promise حل می‌شود؟

کال‌بک هل به ساختار پیچیده و تو در توی توابع کال‌بک گفته می‌شود که خوانایی کد را از بین می‌برد. پرامیس‌ها با امکان زنجیره‌سازی (.then) این مشکل را کاملاً برطرف می‌کنند.

تفاوت متد Promise.all با Promise.race چیست؟

متد Promise.all منتظر موفقیت تمام پرامیس‌ها می‌ماند، اما Promise.race اولین پرامیسی که تعیین تکلیف شود (چه موفق و چه ناموفق) را به‌عنوان نتیجه برمی‌گرداند.

Table of Contents

آخرین نوشته‌ها

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *