درک async/await در جاوااسکریپت
راهنمایی کاربردی دربارهٔ پرامیسها و async/await: چطور کار میکنند، خطاها را چطور مدیریت کنیم و کارها را چطور موازی اجرا کنیم تا برنامه کند نشود.
در این صفحه
جاوااسکریپت کد شما را روی یک رشته (thread) اجرا میکند، با این حال میتواند فایل دانلود کند، منتظر تایمرها بماند و با پایگاه داده حرف بزند بیآنکه قفل شود. راز این کار کد ناهمگام (asynchronous) است و async/await خواناترین راه برای نوشتن آن است.
در این مقاله یاد میگیرید پرامیس واقعاً چیست، async/await چطور روی آن بنا شده، خطاها را چطور درست مدیریت کنید و چطور از رایجترین اشتباهِ عملکردیِ برنامهنویسان دوری کنید.
مسئله: منتظر ماندن بدون مسدود کردن#
فرض کنید میخواهید اطلاعات یک کاربر را از یک API بگیرید. شبکه ممکن است ۳۰۰ میلیثانیه طول بکشد. اگر جاوااسکریپت همانجا میایستاد و صبر میکرد، کل صفحه قفل میشد: نه کلیکی، نه اسکرولی، نه انیمیشنی.
در عوض، توابع ناهمگام بلافاصله یک پرامیس (Promise) برمیگردانند؛ شیئی که نمایندهٔ مقداری است که بعداً در دسترس خواهد بود.
پرامیس در ۶۰ ثانیه#
هر پرامیس همیشه در یکی از سه حالت است:
- pending (در انتظار): کار هنوز در جریان است
- fulfilled (انجامشده): کار با موفقیت و با یک مقدار تمام شده
- rejected (ردشده): کار با یک خطا شکست خورده
با .then() و .catch() به پرامیس واکنش نشان میدهید:
fetch("https://api.github.com/users/octocat")
.then((response) => response.json())
.then((user) => console.log(user.name))
.catch((error) => console.error("Request failed:", error));این روش کار میکند، اما با بزرگتر شدن منطق، دنبال کردن زنجیرهها سخت میشود؛ مخصوصاً وقتی هر مرحله به نتیجهٔ مرحلهٔ قبل وابسته است.
ورود async/await#
async/await سینتکسی است که روی پرامیسها ساخته شده. با آن میتوانید کد ناهمگامی بنویسید که مثل کد همگام خوانده میشود:
async function showUser(username) {
const response = await fetch(`https://api.github.com/users/${username}`);
const user = await response.json();
console.log(user.name);
}
showUser("octocat");دو قانون این کار را ممکن میکند:
asyncقبل از تابع باعث میشود تابع همیشه یک پرامیس برگرداند.awaitهمان تابع را متوقف میکند تا پرامیس به نتیجه برسد و بعد مقدارش را به شما میدهد. در این فاصله بقیهٔ کدها اجرا میشوند؛ فقط همین تابع منتظر میماند.
await صفحه را قفل نمیکند
وقتی showUser روی یک await متوقف است، مرورگر آزاد است کلیکها را پردازش کند، توابع دیگر را اجرا کند و صفحه را دوباره رسم کند. تابع پس از به نتیجه رسیدن پرامیس ادامه پیدا میکند.
مدیریت خطا با try/catch#
در زنجیرههای .then() از .catch() استفاده میکنید. با async/await از همان try/catch آشنا استفاده میکنید:
async function getUser(username) {
try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Could not load ${username}:`, error.message);
return null;
}
}fetch فقط در خطای شبکه رد میشود
پاسخ 404 یا 500 پرامیسِ fetch() را رد نمیکند. همیشه خودتان response.ok (یا response.status) را بررسی کنید، همانطور که بالا دیدید.
اشتباه شمارهٔ ۱: کدی که ناخواسته پشتسرهم اجرا میشود#
به این تابع نگاه کنید. مشکلش را میبینید؟
async function loadDashboard() {
const user = await getUser("octocat"); // ~300 ms
const repos = await getRepos("octocat"); // ~300 ms
const events = await getEvents("octocat"); // ~300 ms
return { user, repos, events }; // total: ~900 ms
}هر درخواست منتظر میماند تا قبلی تمام شود، در حالی که هیچکدام به دیگری وابسته نیستند! همه را همزمان شروع کنید و با Promise.all() با هم منتظرشان بمانید:
async function loadDashboard() {
const [user, repos, events] = await Promise.all([
getUser("octocat"),
getRepos("octocat"),
getEvents("octocat"),
]);
return { user, repos, events }; // total: ~300 ms
}همان نتیجه، تقریباً سه برابر سریعتر.
انتخاب ترکیبکنندهٔ مناسب پرامیس#
| متد | انجام میشود وقتی… | رد میشود وقتی… |
|---|---|---|
Promise.all() |
همهٔ پرامیسها انجام شوند | هر کدام رد شود |
Promise.allSettled() |
همهٔ پرامیسها به نتیجه برسند | هرگز |
Promise.race() |
اولین پرامیسی که به نتیجه میرسد انجام شود | اولین پرامیسی که به نتیجه میرسد رد شود |
Promise.any() |
اولین پرامیس انجام شود | همهٔ پرامیسها رد شوند |
وقتی همهٔ نتیجهها را میخواهید حتی اگر بعضی شکست بخورند، از Promise.allSettled() استفاده کنید؛ مثلاً برای بارگذاری چند ویجت اختیاری:
const results = await Promise.allSettled([getRepos("octocat"), getEvents("octocat")]);
for (const result of results) {
if (result.status === "fulfilled") {
render(result.value);
} else {
console.warn("Widget failed:", result.reason);
}
}افزودن مهلت زمانی (Timeout)#
fetch() بهطور پیشفرض مهلت زمانی ندارد. در مرورگرهای مدرن و Node.js نسخهٔ ۱۸ به بعد، AbortSignal.timeout() آن را در یک خط به شما میدهد:
async function fetchWithTimeout(url, ms = 5000) {
const response = await fetch(url, { signal: AbortSignal.timeout(ms) });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}اگر درخواست بیش از ms میلیثانیه طول بکشد، پرامیس با یک TimeoutError رد میشود که میتوانید مثل هر خطای دیگری در try/catch مدیریتش کنید.
حلقهها و await#
استفاده از await داخل حلقهٔ for...of آیتمها را یکییکی پردازش میکند. این دقیقاً همان چیزی است که وقتی ترتیب مهم است یا نباید یک API را زیر بار ببرید، لازم دارید:
for (const id of orderIds) {
await processOrder(id); // waits for each one before starting the next
}وقتی کارها مستقلاند، آنها را به پرامیس تبدیل کنید و با هم منتظرشان بمانید:
await Promise.all(orderIds.map((id) => processOrder(id)));forEach منتظر نمیماند
orderIds.forEach(async (id) => await processOrder(id)) منتظر هیچ چیزی نمیماند، چون forEach پرامیسهایی را که تابعِ شما برمیگرداند نادیده میگیرد. بهجای آن از for...of یا Promise.all() استفاده کنید.
جمعبندی#
- پرامیس نمایندهٔ یک مقدار در آینده است: در انتظار، انجامشده یا ردشده.
- توابع
asyncهمیشه پرامیس برمیگردانند؛awaitفقط همان تابع را متوقف میکند. - برای خطاها از
try/catchاستفاده کنید و یادتان باشد باfetch()مقدارresponse.okرا بررسی کنید. - کارهای مستقل را با
Promise.all()موازی اجرا کنید، نه اینکه یکییکی منتظرشان بمانید. - هرگز به
forEachبرای منتظر ماندنِ callbackهای ناهمگام تکیه نکنید.
دفعهٔ بعد که await مینویسید، از خودتان بپرسید: آیا این خط واقعاً باید منتظر خط بالایی بماند؟ همین یک سؤال برنامههایتان را بهطور محسوسی سریعتر میکند.