Await و Async در جاوا اسکریپت: مدیریت کدهای ناهمگام به روشی کارآمد

جاوا اسکریپت زبانی تک‌رشته‌ای است، به این معنا که تنها یک کار را در یک زمان می‌تواند انجام دهد. این ویژگی می‌تواند منجر به مشکلاتی در مدیریت عملیات ناهمگام (asynchronous operations) شود؛ عملیاتی که زمانبر هستند و نمی‌توانند فوراً نتیجه را برگردانند، مانند درخواست‌های شبکه، دسترسی به پایگاه داده، یا خواندن و نوشتن فایل. در گذشته، برای مدیریت اینگونه عملیات از Callback و Promise استفاده می‌شد. اما با معرفی async و await در ES2017، جاوا اسکریپت ابزارهای قدرتمندتری برای نوشتن کدهای ناهمگام به روشی خواناتر و مشابه کدهای همگام (synchronous) فراهم کرد.

چرا به async و await نیاز داریم؟

پیش از async و await، مدیریت کدهای ناهمگام اغلب منجر به “Callback Hell” (جهنم Callback) می‌شد؛ وضعیتی که Callbackهای تو در تو کد را دشوار و غیرقابل خواندن می‌کردند. Promiseها تا حدی این مشکل را حل کردند، اما زنجیره کردن Promiseها نیز می‌توانست طولانی و پیچیده شود. async و await با ارائه یک نحو (syntax) تمیزتر و شهودی‌تر، راه حل نهایی برای این چالش‌ها هستند.

async چیست؟

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

مثال:

async function greet() {
  return "سلام جهان!";
}

greet().then(message => console.log(message)); // خروجی: سلام جهان!

async function add(a, b) {
  return a + b;
}

add(5, 3).then(sum => console.log(sum)); // خروجی: ۸

در مثال بالا، تابع greet و add هر دو با async تعریف شده‌اند و در نتیجه Promise برمی‌گردانند. ما می‌توانیم با استفاده از متد .then() به مقدار resolve شده Promise دسترسی پیدا کنیم.

await چیست؟

کلمه کلیدی await تنها می‌تواند درون یک تابع async استفاده شود. await اجرای تابع async را متوقف می‌کند تا زمانی که Promiseای که await بر روی آن اعمال شده، resolve یا reject شود. پس از resolve شدن Promise، await مقدار resolve شده Promise را برمی‌گرداند و اجرای تابع async از سر گرفته می‌شود. در صورت reject شدن Promise، await یک خطا (error) را پرتاب می‌کند که می‌توان آن را با try...catch مدیریت کرد.

مثال:

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("داده‌های دریافت شده از سرور");
    }, ۲۰۰۰); // شبیه سازی تاخیر ۲ ثانیه ای
  });
}

async function processData() {
  console.log("در حال درخواست داده...");
  const data = await fetchData(); // توقف تا زمانی که fetchData resolve شود
  console.log(data);
  console.log("پردازش داده کامل شد.");
}

processData();
// خروجی:
// در حال درخواست داده...
// (۲ ثانیه تاخیر)
// داده‌های دریافت شده از سرور
// پردازش داده کامل شد.

در این مثال، تابع WorkspaceData یک Promise برمی‌گرداند که پس از ۲ ثانیه resolve می‌شود. در تابع processData که یک تابع async است، از await fetchData() استفاده می‌کنیم. این کار باعث می‌شود اجرای processData در این خط متوقف شود تا WorkspaceData Promise خود را resolve کند. پس از آن، مقدار resolve شده به data اختصاص می‌یابد و اجرای تابع ادامه پیدا می‌کند. این الگو کد ناهمگام را بسیار شبیه به کد همگام می‌کند و خواندن آن را آسان‌تر می‌سازد.

مدیریت خطا با try...catch

همانطور که اشاره شد، اگر Promiseای که await بر روی آن اعمال شده reject شود، await یک خطا پرتاب می‌کند. برای مدیریت این خطاها، می‌توانیم از بلوک try...catch استفاده کنیم، درست مانند مدیریت خطاهای همگام.

مثال:

function riskyOperation() {
  return new Promise((resolve, reject) => {
    const success = Math.random() > 0.5; // 50% شانس موفقیت
    setTimeout(() => {
      if (success) {
        resolve("عملیات موفقیت آمیز بود!");
      } else {
        reject("عملیات با شکست مواجه شد.");
      }
    }, ۱۰۰۰);
  });
}

async function performTask() {
  try {
    console.log("در حال انجام وظیفه...");
    const result = await riskyOperation();
    console.log(result);
  } catch (error) {
    console.error("خطا:", error);
  } finally {
    console.log("عملیات به پایان رسید، موفق یا ناموفق.");
  }
}

performTask();

در این مثال، تابع riskyOperation ممکن است resolve یا reject شود. با استفاده از try...catch درون تابع performTask، می‌توانیم هم نتیجه موفقیت آمیز (در بلوک try) و هم خطاهای احتمالی (در بلوک catch) را مدیریت کنیم. بلوک finally نیز همیشه پس از try و catch اجرا می‌شود، صرف نظر از اینکه خطا رخ داده باشد یا نه.

مزایای استفاده از async و await:

  • خوانایی بهتر کد: کدهای ناهمگام را بسیار شبیه به کدهای همگام می‌کند، که درک و نگهداری آن‌ها را آسان‌تر می‌سازد.
  • رفع Callback Hell: از تو در تو شدن Callbackها جلوگیری می‌کند و کد را خطی‌تر می‌کند.
  • مدیریت خطای ساده‌تر: با try...catch می‌توان خطاهای Promiseها را به سادگی مدیریت کرد.
  • دیباگ کردن آسان‌تر: از آنجایی که کد خطی‌تر به نظر می‌رسد، دنبال کردن جریان اجرای آن در هنگام دیباگ کردن نیز ساده‌تر می‌شود.
  • افزایش بهره‌وری: توسعه‌دهندگان می‌توانند با سرعت بیشتری کدهای ناهمگام را بنویسند.

نکات مهم:

  • await فقط می‌تواند درون یک تابع async استفاده شود. تلاش برای استفاده از آن در خارج از یک تابع async باعث خطا می‌شود.
  • همانطور که await اجرای تابع را متوقف می‌کند، باید مراقب باشید که از آن به درستی استفاده کنید تا از مسدود شدن رشته اصلی (main thread) و ایجاد یک تجربه کاربری نامطلوب جلوگیری شود.
  • در صورت نیاز به اجرای چندین Promise به صورت موازی، می‌توان از Promise.all() یا Promise.race() به همراه await استفاده کرد.

نتیجه‌گیری

async و await ابزارهای انقلابی در جاوا اسکریپت هستند که نحوه نوشتن و مدیریت کدهای ناهمگام را به طور چشمگیری بهبود بخشیده‌اند. این ویژگی‌ها با فراهم آوردن یک نحو ساده و خوانا، نه تنها مشکلات گذشته مانند Callback Hell را حل می‌کنند، بلکه امکان نوشتن کدهای ناهمگام قدرتمند و قابل نگهداری را فراهم می‌آورند. با تسلط بر async و await، توسعه‌دهندگان جاوا اسکریپت می‌توانند برنامه‌های کاربردی کارآمدتر و مقیاس‌پذیرتری را توسعه دهند.

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

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