async/await و حلقه‌ی رویداد در جاوااسکریپت؛ چرا ترتیب اجرا گاهی عجیب است
آموزش

async/await و حلقه‌ی رویداد در جاوااسکریپت؛ چرا ترتیب اجرا گاهی عجیب است

کد همزمان همیشه اول اجرا می‌شود، بعد Promiseها، و در آخر setTimeout — حتی با تأخیر صفر. این ترتیب شانسی نیست

تحریریه‌ی دینا کد
نویسنده
۴ دقیقه مطالعه۴ بازدید
اشتراک گذاری۴
فهرست مطالب
  1. ۱یک مثال که ترتیبش عجیب به نظر می‌رسد
  2. ۲async فقط یک Promise برمی‌گرداند
  3. ۳await فقط همان تابع را متوقف می‌کند، نه کل برنامه را
  4. ۴گیر رایج: await داخل forEach
  5. ۵دو راه درست: یکی‌یکی یا همزمان

جواب کوتاه: کد همزمان (sync) همیشه اول اجرا می‌شود، بعد microtaskها (نتیجه‌ی Promiseها)، و در آخر macrotaskها (setTimeout, setInterval) — این ترتیب، نه شانسی است و نه به ترتیب نوشتن کد بستگی دارد.

یک مثال که ترتیبش عجیب به نظر می‌رسد

jsconsole.log('1: شروع');

setTimeout(() => console.log('4: setTimeout'), 0);

Promise.resolve().then(() => console.log('3: promise.then'));

console.log('2: پایان کد همزمان');
1: شروع
2: پایان کد همزمان
3: promise.then
4: setTimeout

با اینکه setTimeout با تأخیر ۰ نوشته شده و قبل از Promise.resolve() هم آمده، بعد از آن اجرا می‌شود. جاوااسکریپت اول تمام کد همزمان را تا انتها اجرا می‌کند (خط ۱ و ۲)، بعد صف microtask را خالی می‌کند (.then)، و فقط در آخر سراغ صف macrotask می‌رود (setTimeout)، حتی با تأخیر صفر.

async فقط یک Promise برمی‌گرداند

jsasync function f() { return 5; }
const r = f();
console.log(r); // Promise { 5 }
r.then(v => console.log('resolved:', v));
console.log('این خط قبل از resolve شدن پرینت می‌شود');
Promise { 5 }
این خط قبل از resolve شدن پرینت می‌شود
resolved: 5

هر async function، مهم نیست چه چیزی return کند، همیشه یک Promise برمی‌گرداند. حتی return 5 هم Promise { 5 } می‌شود، نه خودِ عدد ۵.

await فقط همان تابع را متوقف می‌کند، نه کل برنامه را

jsasync function work() {
  console.log('A: شروع کار async');
  await new Promise(res => setTimeout(res, 50));
  console.log('C: بعد از await');
}
work();
console.log('B: این وسط اجرا می‌شود، بدون توقف');
A: شروع کار async
B: این وسط اجرا می‌شود، بدون توقف
C: بعد از await

await اجرای تابع work را نگه می‌دارد، ولی بقیه‌ی برنامه (خط B) منتظرش نمی‌ماند و بلافاصله اجرا می‌شود. این دقیقاً همان چیزی است که جاوااسکریپت را non-blocking نگه می‌دارد.

گیر رایج: await داخل forEach

js[300, 100, 200].forEach(async (ms) => {
  await delay(ms, ms);
});
console.log('این خط بلافاصله پرینت می‌شود، بدون صبر کردن');
این خط بلافاصله پرینت می‌شود، بدون صبر کردن
انجام شد: 100
انجام شد: 200
انجام شد: 300

forEach منتظر Promiseهایی که callbackاش برمی‌گرداند نمی‌ماند — چون خروجی callback را اصلاً دور می‌ریزد. اگر واقعاً به «صبر کردن تا همه تمام شوند» نیاز داری، باید از for...of یا Promise.all استفاده کنی.

دو راه درست: یکی‌یکی یا همزمان

js// یکی‌یکی — هرکدام منتظر قبلی می‌ماند
for (const ms of [300, 100, 200]) {
  await delay(ms, ms);
}
// انجام شد: 300, 100, 200 — به همین ترتیب
js// همزمان — همه باهم شروع می‌شوند، ولی کد صبر می‌کند تا همه تمام شوند
const results = await Promise.all([300, 100, 200].map(ms => delay(ms, ms)));
console.log(results); // [300, 100, 200]

نکته‌ی ظریف اینجاست: با Promise.all، هرکدام زودتر تمام شود زودتر لاگ می‌شود (۱۰۰ اول تمام می‌شود)، ولی آرایه‌ی نتیجه همیشه به ترتیب همان آرایه‌ی ورودی است (۳۰۰، ۱۰۰، ۲۰۰)، نه به ترتیب اتمام.

اگر می‌خواهید جاوااسکریپت را از پایه و با تمرین واقعی یاد بگیرید — از مقدارهای اولیه تا همین حلقه‌ی رویداد — دوره‌ی جاوااسکریپت از صفر دقیقاً همین رفتارها را قدم‌به‌قدم تمرین می‌دهد.

تحریریه‌ی دینا کد
نویسنده

نظرها

اولین نفری باشید که نظر می‌دهد
برای ثبت نظر وارد شوید

سؤالتان را بپرسید یا تجربه‌تان را بنویسید تا نویسنده‌ی مطلب پاسخ بدهد.

مطالب مرتبط

از همین دسته
دستور stash در گیت چیست و چه کاربردی دارد
آموزش

دستور stash در گیت چیست و چه کاربردی دارد

دستور stash در گیت تغییرات ثبت‌نشده شاخه کاری را موقتا کنار می‌گذارد تا بدون ثبت کامیت ناقص بتوانید روی وظایف فوری کار کنید.

تحریریه‌ی دینا کد۶ دقیقه
diff در گیت چیست و چه‌وقت استفاده می‌شود؟
آموزش

diff در گیت چیست و چه‌وقت استفاده می‌شود؟

git diff دو نسخه از کدها را کنار هم می‌گذارد و خط‌به‌خط نشان می‌دهد چه چیزی عوض شده. یک نگاه به diff قبل از هر add و commit کافی است

تحریریه‌ی دینا کد۷ دقیقه