آموزش Async و Await در جاوا اسکریپت

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

در دنیای امروز که سرعت توسعه نرم‌افزار حرف اول را می‌زند، تسلط بر ابزارهای قدرتمند و کارآمد از اهمیت بالایی برخوردار است. جاوا اسکریپت، به عنوان یکی از پرکاربردترین زبان‌های برنامه‌نویسی وب، همواره در حال تکامل است و هر نسخه جدید، قابلیت‌های تازه‌ای را برای ساده‌سازی فرآیندهای توسعه به ارمغان می‌آورد. یکی از این قابلیت‌های مهم و تأثیرگذار، مفهوم Async و Await است که از ES2017 (ES8) به این زبان اضافه شد. این دو کلمه کلیدی، که بر پایه Promiseها بنا شده‌اند، راه حلی زیبا و کارآمد برای مدیریت عملیات ناهمگام ارائه می‌دهند و پیچیدگی‌های ناشی از Callback Hell یا زنجیره‌های طولانی Promise.then() را به میزان قابل توجهی کاهش می‌دهند.

در گذشته، توسعه‌دهندگان جاوا اسکریپت برای مدیریت عملیات‌هایی مانند فراخوانی API، خواندن و نوشتن فایل یا هرگونه عملیات زمان‌بر دیگر، با چالش‌های فراوانی روبرو بودند. Callbackها اولین راه حل بودند که به سرعت منجر به مشکلی به نام “Callback Hell” یا “هرم مرگ Callback” می‌شدند، جایی که کدها به دلیل تو در تو شدن زیاد، غیرقابل خواندن و نگهداری می‌شدند. Promiseها گامی بزرگ رو به جلو بودند و با ارائه ساختاری منظم‌تر، این چالش‌ها را تا حد زیادی برطرف کردند، اما زنجیره‌های طولانی .then().then().catch() نیز خود می‌توانستند خوانایی کد را تحت تأثیر قرار دهند. اینجا بود که Async و Await وارد میدان شدند تا با ارائه سینتکسی شبیه به کد همگام، اما با حفظ طبیعت ناهمگام، راه‌حلی نهایی و قدرتمند برای این مسائل ارائه دهند.

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

درک برنامه‌نویسی ناهمگام در جاوا اسکریپت

قبل از پرداختن به جزئیات Async و Await، لازم است تا درک درستی از مفهوم برنامه‌نویسی ناهمگام و چرایی اهمیت آن در جاوا اسکریپت داشته باشیم. جاوا اسکریپت ذاتاً یک زبان تک‌رشته‌ای (Single-threaded) است، به این معنا که تنها یک کار را در یک زمان می‌تواند انجام دهد. این ویژگی، در نگاه اول، ممکن است یک محدودیت به نظر برسد، اما با مکانیزم‌های هوشمندانه‌ای مانند Event Loop، Call Stack و Callback Queue، جاوا اسکریپت توانسته است عملیات‌های ناهمگام را به شکلی کارآمد مدیریت کند.

جاوا اسکریپت تک‌رشته‌ای چیست؟

مفهوم تک‌رشته‌ای بودن جاوا اسکریپت به این معنی است که در هر لحظه، تنها یک قطعه کد در حال اجرا است. Call Stack جایی است که توابع به ترتیب فراخوانی روی هم قرار می‌گیرند و پس از اتمام اجرا، از آن خارج می‌شوند. اما وقتی یک عملیات زمان‌بر مانند درخواست شبکه یا تایمر اجرا می‌شود، این عملیات از Call Stack خارج شده و به APIهای مرورگر یا Node.js منتقل می‌شود. پس از اتمام این عملیات زمان‌بر، نتیجه آن به Callback Queue اضافه می‌شود و Event Loop مسئولیت نظارت بر Call Stack و Callback Queue را بر عهده دارد. هر زمان که Call Stack خالی باشد، Event Loop اولین تابع را از Callback Queue برداشته و به Call Stack منتقل می‌کند تا اجرا شود. این چرخه تضمین می‌کند که رابط کاربری (UI) مسدود نمی‌شود و برنامه پاسخگو باقی می‌ماند.

مروری بر روش‌های سنتی مدیریت ناهمگامی

پیش از ظهور Async/Await، توسعه‌دهندگان از روش‌های مختلفی برای مدیریت عملیات ناهمگام استفاده می‌کردند که هر کدام مزایا و چالش‌های خاص خود را داشتند.

Callbacks

Callbacks اولین و پایه‌ای‌ترین روش برای مدیریت ناهمگامی بودند. در این الگو، تابعی را به عنوان آرگومان به تابع دیگری پاس می‌دهیم و پس از اتمام عملیات زمان‌بر، تابع Callback فراخوانی می‌شود. این روش، هرچند ساده است، اما در صورت نیاز به اجرای چندین عملیات ناهمگام به صورت متوالی و وابسته به یکدیگر، منجر به پدیده‌ای به نام “Callback Hell” یا “هرم مرگ Callback” می‌شود.

Callback Hell یا هرم مرگ Callback به وضعیتی اشاره دارد که در آن چندین تابع Callback تو در تو به کار می‌روند و خوانایی و نگهداری کد را به شدت دشوار می‌سازند، که نشان‌دهنده نیاز به راه‌حل‌های ساختاریافته‌تر برای مدیریت ناهمگامی است.

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

Promises

Promises به عنوان یک راه‌حل ساختاریافته‌تر برای Callback Hell معرفی شدند. یک Promise نماینده‌ای برای یک عملیات ناهمگام است که در آینده نتیجه‌ای را برمی‌گرداند (موفقیت‌آمیز یا خطا). Promises سه حالت دارند: Pending (در حال انتظار)، Fulfilled (موفقیت‌آمیز) و Rejected (خطا). متدهای `.then()`، `.catch()` و `.finally()` به ما این امکان را می‌دهند که به ترتیب، به نتایج موفقیت‌آمیز، خطاها و اتمام عملیات (فارغ از موفقیت یا خطا) واکنش نشان دهیم. Promiseها با زنجیره‌سازی امکان نوشتن کدهای خواناتر را فراهم کردند، اما در سناریوهای پیچیده با چندین عملیات متوالی، زنجیره‌های طولانی Promise نیز می‌توانستند چالش‌برانگیز شوند و گاهی اوقات برای خوانایی به مشکل می‌خوردند.

Async و Await: انقلابی در کدهای ناهمگام

Async و Await، که در ES2017 معرفی شدند، نه تنها یک ویژگی جدید هستند، بلکه یک “Syntactic Sugar” قدرتمند بر روی Promises محسوب می‌شوند. هدف اصلی آن‌ها، ساده‌سازی نوشتن کدهای ناهمگام به گونه‌ای است که بسیار شبیه به کدهای همگام (Synchronous) به نظر برسند و خوانایی فوق‌العاده‌ای داشته باشند، بدون اینکه Main Thread جاوا اسکریپت را مسدود کنند.

تصور کنید در یک کافی‌شاپ بزرگ، یک باریستا وظیفه آماده کردن انواع قهوه را دارد. اگر باریستا تنها یک سفارش را در یک زمان آماده کند و تا پایان آن منتظر بماند، صف مشتریان طولانی خواهد شد. اما یک باریستای حرفه‌ای، همزمان که قهوه یک مشتری در حال دم کشیدن است، می‌تواند سفارش مشتری بعدی را آماده کند، مواد لازم را فراهم آورد یا حتی ظرف‌ها را بشوید. Async و Await دقیقاً همین نقش را در کدنویسی جاوا اسکریپت ایفا می‌کنند؛ برنامه می‌تواند منتظر بماند تا یک عملیات ناهمگام به پایان برسد، بدون اینکه در این مدت، کل برنامه را متوقف کند. این رویکرد، پویایی و پاسخگویی برنامه را تضمین می‌کند و تجربه کاربری بسیار بهتری را به ارمغان می‌آورد.

کلمه کلیدی `async` و قدرت آن

کلمه کلیدی `async` را می‌توان قبل از تعریف هر تابعی قرار داد و معنای ساده‌ای دارد: یک تابع `async` همواره یک Promise را برمی‌گرداند. اگر تابع `async` یک مقدار معمولی (غیر Promise) را برگرداند، جاوا اسکریپت به طور خودکار آن را در یک Promise حل شده (Resolved Promise) قرار می‌دهد و آن را برمی‌گرداند. اگر تابع `async` یک خطا (Exception) ایجاد کند، آن خطا به یک Promise رد شده (Rejected Promise) تبدیل می‌شود.

سینتکس و کاربرد `async` function

سینتکس یک تابع `async` به سادگی به شرح زیر است:

async function myFunction() { // کدهای ناهمگام } // یا به صورت Arrow Function const myAsyncFunction = async () => { // کدهای ناهمگام };

با قرار دادن کلمه `async` قبل از یک تابع، به جاوا اسکریپت اعلام می‌کنیم که این تابع حاوی عملیات‌های ناهمگام خواهد بود و نتیجه نهایی آن یک Promise است. این تضمین، پایه‌ای برای استفاده از `await` در داخل آن تابع فراهم می‌کند.

چگونه `async` یک Promise برمی‌گرداند؟

همانطور که گفته شد، توابع `async` همیشه یک Promise را برمی‌گردانند. بیایید با چند مثال این مفهوم را روشن‌تر کنیم:

async function greeting() { return “سلام جهان!”; } greeting().then(alert); // خروجی: “سلام جهان!” (در یک alert box)

در این مثال، تابع `greeting` یک رشته معمولی را برمی‌گرداند، اما چون با `async` تعریف شده است، خروجی آن یک Promise حل شده است که مقدار “سلام جهان!” را در خود دارد. می‌توانیم به وضوح یک Promise را نیز برگردانیم:

async function fetchUser() { return new Promise(resolve => { setTimeout(() => resolve({ id: 1, name: “علی” }), 1000); }); } fetchUser().then(user => console.log(user.name)); // پس از 1 ثانیه: “علی”

این نشان می‌دهد که `async` به صورت خودکار مقادیر غیر Promise را به Promise تبدیل می‌کند و به ما اطمینان می‌دهد که همیشه با یک Promise سر و کار داریم، که این یکپارچگی در مدیریت جریان ناهمگام بسیار مفید است.

کلمه کلیدی `await` و توقف هوشمندانه

کلمه کلیدی `await` یک مفهوم جادویی است که فقط در داخل توابع `async` قابل استفاده است. وظیفه اصلی `await` این است که اجرای تابع `async` را تا زمانی که یک Promise حل (resolve) یا رد (reject) شود، “متوقف” می‌کند. این توقف، Main Thread جاوا اسکریپت را مسدود نمی‌کند، بلکه به Event Loop اجازه می‌دهد تا در این فاصله کارهای دیگری را انجام دهد و برنامه را پاسخگو نگه دارد.

سینتکس و شرط استفاده از `await`

سینتکس `await` بسیار ساده است:

let result = await somePromise();

مهم‌ترین شرط برای استفاده از `await` این است که حتماً باید درون یک تابع `async` قرار گیرد. اگر سعی کنید از `await` در یک تابع معمولی استفاده کنید، جاوا اسکریپت خطای سینتکسی (Syntax Error) را گزارش خواهد داد. این محدودیت تضمین می‌کند که تمامی Promiseهای `await` شده به درستی مدیریت می‌شوند.

عملکرد `await` در عمل

زمانی که `await` قبل از یک فراخوانی Promise قرار می‌گیرد، جاوا اسکریپت اجرای تابع `async` را در آن نقطه به حالت تعلیق درمی‌آورد. کد تا زمانی که Promise مورد نظر به نتیجه برسد (حل شود یا خطا دهد) منتظر می‌ماند. سپس، اگر Promise حل شده باشد، `await` مقدار حل شده را برمی‌گرداند و اگر Promise رد شده باشد، `await` یک استثنا (Exception) ایجاد می‌کند که می‌توان آن را با `try…catch` مدیریت کرد.

function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function showMessage() { console.log(“شروع”); await delay(2000); // اجرای تابع 2 ثانیه متوقف می‌شود console.log(“پایان پس از 2 ثانیه”); } showMessage();

در این مثال، `console.log(“پایان پس از 2 ثانیه”)` تنها پس از گذشت 2 ثانیه از `delay(2000)` اجرا می‌شود. اما در این دو ثانیه، جاوا اسکریپت بیکار نمی‌ماند و می‌تواند به پردازش سایر رویدادها یا اجرای کدهای دیگر بپردازد. این قابلیت، یکی از دلایل اصلی محبوبیت و کارایی `await` است؛ زیرا باعث می‌شود کدهای ناهمگام به صورت خطی و خواناتر نوشته شوند.

مقایسه Async/Await با Promises و Callbacks: از پیچیدگی تا سادگی

برای درک بهتر برتری Async/Await، ضروری است که آن را در یک سناریوی عملی با Callbacks و Promises مقایسه کنیم. این مقایسه به وضوح نشان می‌دهد که چگونه Async/Await کدهای ناهمگام را ساده‌تر، خواناتر و قابل نگهداری‌تر می‌سازد.

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

پیاده‌سازی با Callbacks (Callback Hell)

در ابتدا، پیاده‌سازی این سناریو با Callbacks، تصویری از پیچیدگی Callback Hell را نشان می‌دهد:

function fetchData(callback) { setTimeout(() => { console.log(“داده دریافت شد.”); callback(null, { id: 1, value: “initial data” }); }, 1000); } function processData(data, callback) { setTimeout(() => { console.log(“داده پردازش شد.”); callback(null, { …data, processed: true }); }, 800); } function sendData(processedData, callback) { setTimeout(() => { console.log(“داده ارسال شد.”); callback(null, “success”); }, 700); } fetchData((err, data) => { if (err) { console.error(err); return; } processData(data, (err, processedData) => { if (err) { console.error(err); return; } sendData(processedData, (err, result) => { if (err) { console.error(err); return; } console.log(“عملیات کامل: ” + result); }); }); });

همانطور که مشاهده می‌کنید، کد به صورت تو در تو نوشته شده و مدیریت خطا در هر مرحله نیازمند بلوک‌های `if (err)` جداگانه است که به سرعت خوانایی کد را از بین می‌برد. این همان “Callback Hell” است.

پیاده‌سازی با Promises

Promiseها این وضعیت را تا حد زیادی بهبود می‌بخشند و کد را خطی‌تر و خواناتر می‌کنند:

function fetchDataPromise() { return new Promise(resolve => { setTimeout(() => { console.log(“داده دریافت شد (Promise).”); resolve({ id: 1, value: “initial data” }); }, 1000); }); } function processDataPromise(data) { return new Promise(resolve => { setTimeout(() => { console.log(“داده پردازش شد (Promise).”); resolve({ …data, processed: true }); }, 800); }); } function sendDataPromise(processedData) { return new Promise(resolve => { setTimeout(() => { console.log(“داده ارسال شد (Promise).”); resolve(“success”); }, 700); }); } fetchDataPromise() .then(data => processDataPromise(data)) .then(processedData => sendDataPromise(processedData)) .then(result => console.log(“عملیات کامل (Promise): ” + result)) .catch(error => console.error(“خطا (Promise):”, error));

این نسخه بسیار خواناتر از Callback Hell است، اما همچنان با زنجیره‌ای از `.then()` روبرو هستیم که در پروژه‌های بزرگ می‌تواند طولانی و کمی خسته‌کننده شود، به ویژه زمانی که نیاز به مدیریت خطاهای پیچیده‌تر باشد.

پیاده‌سازی با Async/Await

حالا نوبت به Async/Await می‌رسد که کد را به شکلی حیرت‌انگیز ساده و شبیه به کد همگام می‌سازد:

async function performOperations() { try { console.log(“شروع عملیات با Async/Await”); const data = await fetchDataPromise(); const processedData = await processDataPromise(data); const result = await sendDataPromise(processedData); console.log(“عملیات کامل (Async/Await): ” + result); } catch (error) { console.error(“خطا (Async/Await):”, error); } } performOperations();

همانطور که می‌بینید، کد با Async/Await نه تنها به طرز چشمگیری خواناتر شده و شبیه به یک کد همگام خطی به نظر می‌رسد، بلکه مدیریت خطا نیز با بلوک `try…catch` به مراتب ساده‌تر و متمرکزتر انجام می‌شود. این سادگی و خوانایی، دلیل اصلی ترجیح Async/Await در توسعه مدرن جاوا اسکریپت است.

در ادامه، خلاصه‌ای از مقایسه این سه روش را در یک جدول مشاهده می‌کنید:

ویژگی Callbacks Promises Async/Await
خوانایی کد پایین (Callback Hell) متوسط تا خوب عالی (شبیه به کد همگام)
مدیریت خطا پیچیده (نیاز به بررسی در هر مرحله) نسبتاً خوب (.catch()) عالی (try…catch)
سینتکس تو در تو، غیرخطی زنجیره‌ای (.then()) خطی، واضح
پیچیدگی زیاد در عملیات‌های متوالی متوسط پایین
پشتیبانی اولیه، در تمام نسخه‌ها ES6 (2015) به بعد ES2017 (ES8) به بعد

مدیریت خطا در Async/Await: ایمنی و پایداری کد

یکی از مزایای برجسته Async و Await، ساده‌سازی مدیریت خطا در عملیات ناهمگام است. با استفاده از بلوک `try…catch`، می‌توانیم خطاهایی را که از Promiseهای `await` شده ایجاد می‌شوند، به همان روشی که خطاهای همگام را مدیریت می‌کنیم، کنترل کنیم. این رویکرد، کد را بسیار خواناتر و نگهداری‌پذیرتر می‌سازد.

استفاده از `try…catch`

وقتی `await` یک Promise را که رد (Rejected) شده است دریافت می‌کند، یک استثنا (Exception) ایجاد می‌کند. این استثنا دقیقاً مانند یک خطای پرتاب شده معمولی عمل می‌کند و می‌تواند توسط یک بلوک `try…catch` گرفته شود. این قابلیت به ما اجازه می‌دهد تا منطق مدیریت خطا را به صورت متمرکز در یک مکان مشخص در تابع `async` خود قرار دهیم.

async function getUserData(userId) { try { const response = await fetch(`https://api.example.com/users/${userId}`); if (!response.ok) { throw new Error(`خطای شبکه: ${response.status}`); } const data = await response.json(); console.log(“اطلاعات کاربر:”, data); } catch (error) { console.error(“مشکلی در دریافت اطلاعات کاربر پیش آمد:”, error.message); } } getUserData(1); // مثال موفقیت‌آمیز getUserData(999); // فرض کنید این ID خطا می‌دهد

در این مثال، اگر `fetch` با خطای شبکه مواجه شود یا پاسخ `response.ok` نباشد، یک خطا پرتاب می‌شود که توسط بلوک `catch` مدیریت می‌گردد. این روش باعث می‌شود کد مدیریت خطا بسیار شبیه به کدهای همگام به نظر برسد و از پیچیدگی‌های مربوط به `.catch()` در زنجیره‌های Promise جلوگیری می‌کند.

ارتباط با `Promise.catch()` و `unhandledrejection`

گرچه `try…catch` روش اصلی مدیریت خطا در Async/Await است، اما گاهی اوقات ممکن است همچنان به `.catch()` نیاز داشته باشیم، به خصوص در بالاترین سطح فراخوانی که تابع `async` را فراخوانی می‌کنیم و ممکن است خودمان در یک تابع `async` نباشیم. اگر یک تابع `async` خطایی را ایجاد کند و آن خطا توسط `try…catch` داخلی مدیریت نشود، Promise‌ای که آن تابع `async` برمی‌گرداند، رد خواهد شد. در این صورت، برای مدیریت این خطا، باید یک `.catch()` به فراخوانی تابع `async` در بیرون اضافه کنیم:

async function problematicFunction() { throw new Error(“این یک خطاست!”); } problematicFunction() .catch(error => console.error(“خطای مدیریت شده با .catch():”, error.message));

همچنین، اگر یک Promise رد شود و هیچ `.catch()` یا `try…catch` برای آن وجود نداشته باشد، یک خطای `unhandledrejection` ایجاد می‌شود. این خطاها معمولاً در کنسول مرورگر نمایش داده می‌شوند و می‌توانند با استفاده از یک Event Listener سراسری برای `unhandledrejection` مدیریت شوند تا از از دست رفتن خطاهای مهم جلوگیری شود:

window.addEventListener(‘unhandledrejection’, event => { console.error(‘Promise مدیریت نشده:’, event.promise, event.reason); });

تکنیک‌های پیشرفته با Async/Await: فراتر از اصول اولیه

پس از درک مفاهیم پایه Async و Await، می‌توانیم به سراغ تکنیک‌های پیشرفته‌تری برویم که کارایی و انعطاف‌پذیری کدهای ناهمگام را بیش از پیش افزایش می‌دهند.

اجرای همزمان چندین Promise

یکی از نیازمندی‌های رایج در برنامه‌نویسی ناهمگام، اجرای همزمان چندین عملیات است که به یکدیگر وابسته نیستند. `Promise.all` همراه با `await` بهترین راه حل برای این سناریو است.

`Promise.all` با `await`

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

async function fetchMultipleData() { try { const [users, products] = await Promise.all([ fetch(‘https://api.example.com/users’).then(res => res.json()), fetch(‘https://api.example.com/products’).then(res => res.json()) ]); console.log(“کاربران:”, users); console.log(“محصولات:”, products); } catch (error) { console.error(“خطا در دریافت داده‌ها:”, error); } } fetchMultipleData();

`Promise.race`, `Promise.any`, `Promise.allSettled` با `await`

  • `Promise.race` با `await`: این تابع، Promiseای را برمی‌گرداند که به محض حل یا رد شدن یکی از Promiseهای ورودی، حل یا رد شود. زمانی که فقط نتیجه اولین Promise را نیاز داریم، مفید است.
  • `Promise.any` با `await`: Promiseای را برمی‌گرداند که به محض حل شدن اولین Promise ورودی، حل می‌شود. اگر تمام Promiseهای ورودی رد شوند، یک `AggregateError` پرتاب می‌کند.
  • `Promise.allSettled` با `await`: این تابع، Promiseای را برمی‌گرداند که پس از اتمام (حل یا رد شدن) تمام Promiseهای ورودی حل می‌شود. نتیجه آن یک آرایه از اشیاء است که وضعیت و مقدار/دلیل رد شدن هر Promise را نشان می‌دهد، صرف نظر از اینکه موفق بوده یا شکست خورده است. این برای زمانی مناسب است که می‌خواهیم تمام نتایج را داشته باشیم، حتی اگر برخی از Promiseها خطا داده باشند.

Top-Level Await: قدرت در ماژول‌ها

تا پیش از این، `await` فقط در داخل توابع `async` قابل استفاده بود. اما با معرفی Top-Level Await در ES2022، اکنون می‌توانیم از `await` مستقیماً در خارج از یک تابع `async`، در سطح بالای یک ماژول جاوا اسکریپت (ES Module)، استفاده کنیم. این قابلیت به ویژه در سناریوهایی مانند بارگذاری پویا ماژول‌ها یا آماده‌سازی منابع قبل از اجرای کد اصلی، بسیار مفید است.

// در یک فایل ماژول (.mjs یا در package.json با type: “module”) // const response = await fetch(‘https://api.example.com/config’); // const config = await response.json(); // console.log(‘تنظیمات برنامه:’, config); // export const API_KEY = config.apiKey;

برای محیط‌هایی که از Top-Level Await پشتیبانی نمی‌کنند یا برای حفظ سازگاری با مرورگرهای قدیمی، می‌توان از یک Immediately Invoked Function Expression (IIFE) `async` استفاده کرد:

(async () => { // کدهای دارای await در اینجا const data = await fetchDataPromise(); console.log(“داده در IIFE:”, data); })();

Async Class Methods: معماری مدرن با Async

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

class DataService { constructor(baseUrl) { this.baseUrl = baseUrl; } async fetchItem(id) { try { const response = await fetch(`${this.baseUrl}/items/${id}`); if (!response.ok) { throw new Error(`خطا: ${response.status}`); } return await response.json(); } catch (error) { console.error(`مشکل در دریافت آیتم ${id}:`, error); throw error; // دوباره خطا را پرتاب می‌کنیم تا فراخواننده نیز مطلع شود } } } const service = new DataService(‘https://api.example.com’); service.fetchItem(123).then(item => console.log(item));

Thenables: سازگاری `await` با اشیاء مشابه Promise

`await` نه تنها با Promiseهای بومی جاوا اسکریپت کار می‌کند، بلکه با هر شیئی که دارای متد `.then()` باشد (معروف به Thenable) نیز سازگار است. این قابلیت انعطاف‌پذیری بالایی را فراهم می‌کند و به `await` اجازه می‌دهد تا با کتابخانه‌ها و APIهای سفارشی که از الگوی Promise پیروی می‌کنند، به خوبی کار کند، حتی اگر آن‌ها صراحتاً Promise بومی نباشند.

class CustomThenable { constructor(value, delay) { this.value = value; this.delay = delay; } then(resolve, reject) { console.log(`انتظار برای Thenable با مقدار ${this.value} به مدت ${this.delay} میلی‌ثانیه…`); setTimeout(() => resolve(this.value), this.delay); } } async function useThenable() { const result1 = await new CustomThenable(“اولین نتیجه”, 1000); console.log(result1); const result2 = await new CustomThenable(“دومین نتیجه”, 500); console.log(result2); } useThenable(); // خروجی: پس از 1 ثانیه “اولین نتیجه” و پس از 0.5 ثانیه “دومین نتیجه”

بهترین شیوه‌ها (Best Practices) در استفاده از Async/Await: کدنویسی حرفه‌ای

برای بهره‌برداری حداکثری از Async/Await و نوشتن کدهای تمیز، کارآمد و قابل نگهداری، رعایت بهترین شیوه‌ها ضروری است.

  1. همیشه `await` را در `try…catch` بپیچید:برای مدیریت صحیح خطاها و جلوگیری از `unhandledrejection`، هر بلوک کد که شامل `await` است و پتانسیل خطا دارد، باید درون `try…catch` قرار گیرد.
  2. از Parallelization با `Promise.all` برای بهبود عملکرد استفاده کنید:اگر چندین عملیات ناهمگام به یکدیگر وابسته نیستند و می‌توانند به صورت همزمان اجرا شوند، آن‌ها را با `Promise.all` ترکیب کنید تا زمان اجرای کلی کاهش یابد.
  3. از `async` در توابع کوچک و متمرکز برای بهبود قابلیت نگهداری استفاده کنید:توابع `async` را به گونه‌ای طراحی کنید که مسئولیت واحدی داشته باشند. این کار باعث می‌شود کد شما ماژولارتر و تست‌پذیرتر شود.
  4. اجتناب از استفاده بیش از حد و پشت سر هم از `await` در حلقه‌ها:اگر در یک حلقه (مانند `for` یا `forEach`) به طور مکرر از `await` استفاده کنید، عملیات‌ها به صورت متوالی اجرا می‌شوند و ممکن است عملکرد برنامه را کاهش دهد. در چنین مواردی، `Promise.all` یا `Promise.allSettled` را برای اجرای موازی در نظر بگیرید.
  5. استفاده از `finally` در `try…catch…finally` برای پاکسازی منابع:بلوک `finally` تضمین می‌کند که کدهای مربوط به پاکسازی (مانند بستن اتصال به پایگاه داده یا توقف لودینگ ایندیکاتور) همیشه اجرا می‌شوند، صرف نظر از اینکه عملیات موفقیت‌آمیز بوده یا با خطا مواجه شده است.
  6. نام‌گذاری مناسب توابع `async`:نام توابع `async` باید به وضوح نشان‌دهنده عملیات ناهمگامی باشند که انجام می‌دهند (مثلاً `fetchUserData`, `saveSettings`, `uploadFile`).
  7. مدیریت زمان‌بندی (Timeout) برای عملیات‌های `await`: برای جلوگیری از مسدود شدن طولانی مدت برنامه در انتظار یک Promise که هرگز حل نمی‌شود، می‌توانید Promiseهای خود را با یک Promise دیگر که پس از یک زمان مشخص رد می‌شود (Timeout Promise)، ترکیب کنید.

مجتمع فنی تهران: پیشرو در آموزش جاوا اسکریپت

با توجه به اهمیت فزاینده جاوا اسکریپت و نقش حیاتی آن در توسعه وب مدرن، تسلط بر مفاهیم پیشرفته‌ای مانند Async و Await برای هر توسعه‌دهنده‌ای ضروری است. این دانش نه تنها شما را در بازار کار رقابتی‌تر می‌کند، بلکه توانایی شما را در ساخت برنامه‌های قوی‌تر و کارآمدتر افزایش می‌دهد.

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

چه به دنبال آموزش مقدماتی تا پیشرفته جاوا اسکریپت باشید و چه قصد داشته باشید مهارت‌های خود را در زمینه‌های خاصی مانند Async/Await تقویت کنید، دوره‌های ما طوری طراحی شده‌اند که نیازهای شما را برآورده سازند. تمرکز ما بر آموزش javascript پروژه محور است، به این معنی که شما با ساخت پروژه‌های واقعی، مفاهیم را عملاً تجربه کرده و به یادگیری عمیق‌تری دست پیدا می‌کنید. این رویکرد به شما کمک می‌کند تا پس از اتمام دوره، با اعتماد به نفس کامل وارد بازار کار شوید و دانش آموزش JavaScript خود را به کار بگیرید.

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

تمرین‌ها و مثال‌های عملی تکمیلی

برای تثبیت یادگیری و درک عمیق‌تر Async/Await، انجام تمرین‌های عملی از اهمیت بالایی برخوردار است. در اینجا چند سناریو و راه‌حل آن‌ها را برای شما آورده‌ایم:

بازنویسی یک تابع از Promise.then به Async/Await

فرض کنید تابعی به نام `loadJson` داریم که از Promise برای بارگذاری داده‌ها استفاده می‌کند:

function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new Error(response.status); } }); } loadJson(‘https://api.example.com/data’) .then(data => console.log(data)) .catch(error => console.error(error));

حال، این تابع را با استفاده از Async/Await بازنویسی می‌کنیم:

async function loadJsonAsync(url) { let response = await fetch(url); if (response.status == 200) { return await response.json(); } else { throw new Error(response.status); } } loadJsonAsync(‘https://api.example.com/data’) .then(data => console.log(data)) .catch(error => console.error(“خطا در بارگذاری داده:”, error.message));

همانطور که می‌بینید، کد `loadJsonAsync` به وضوح خواناتر است و جریان منطقی آن شبیه به کد همگام است. استفاده از `await` قبل از `fetch` و `response.json()` باعث می‌شود که هر مرحله به صورت خطی اجرا شود و مدیریت خطا نیز به سادگی با `throw new Error` انجام می‌گیرد.

فراخوانی غیرهمگام از غیر همگام (Async from non-async)

چگونه می‌توان یک تابع `async` را از یک تابع معمولی (non-async) فراخوانی کرد و از نتیجه آن استفاده نمود؟

async function getGreeting() { await new Promise(resolve => setTimeout(resolve, 1000)); return “سلام از تابع async!”; } function displayGreeting() { getGreeting().then(message => { console.log(message); }); console.log(“این خط بلافاصله اجرا می‌شود.”); } displayGreeting();

در این مثال، تابع `displayGreeting` یک تابع معمولی است. برای فراخوانی `getGreeting` (که یک تابع `async` است و Promise برمی‌گرداند)، از `.then()` استفاده می‌کنیم تا نتیجه Promise را دریافت کنیم. این تضمین می‌کند که حتی در توابع غیر `async` نیز می‌توانیم با نتایج عملیات‌های ناهمگام کار کنیم.

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

آیا Async/Await عملکرد برنامه را بهبود می‌بخشد یا صرفاً خوانایی کد را افزایش می‌دهد؟

Async/Await عمدتاً خوانایی و نگهداری‌پذیری کد را افزایش می‌دهد، اما مستقیماً عملکرد اجرایی جاوا اسکریپت را بهبود نمی‌بخشد؛ این ابزار فقط یک سینتکس بهتر برای Promises است.

چگونه می‌توان از `await` در یک تابع همگام (synchronous function) استفاده کرد (بدون استفاده از top-level await در ماژول‌ها)؟

برای استفاده از `await`، تابع باید `async` باشد. در یک تابع همگام، باید از `.then()` برای مدیریت Promise استفاده کنید یا تابع همگام را به `async` تبدیل کنید.

آیا استفاده از Promise.all با Async/Await همیشه بهترین راه برای اجرای موازی چندین تسک ناهمگام است؟

بله، Promise.all بهترین راه برای اجرای موازی چندین Promise است که برای موفقیت به یکدیگر وابسته هستند و نیاز داریم تمام نتایج آن‌ها را دریافت کنیم.

در چه مواردی بهتر است همچنان از Callbacks یا `Promise.then/.catch` به جای Async/Await استفاده کنیم؟

در برخی موارد بسیار ساده و کوتاه، یا برای حفظ سازگاری با کدهای قدیمی، ممکن است همچنان از Callbacks یا `.then/.catch` استفاده شود، اما به طور کلی Async/Await ارجحیت دارد.

بهترین روش برای دیباگ کردن کدهای Async/Await و مدیریت Call Stack در آن‌ها چیست؟

بهترین روش استفاده از قابلیت‌های دیباگر مرورگرها یا Node.js است که Call Stack ناهمگام را به خوبی نمایش می‌دهند؛ همچنین، استفاده صحیح از `try…catch` به شناسایی منبع خطا کمک می‌کند.

دکمه بازگشت به بالا