آموزش

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

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

تحریریه‌ی دینا کد
نویسنده
۴ دقیقه مطالعه۰ بازدید
اشتراک گذاری۰

جواب کوتاه: کد همزمان (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، هرکدام زودتر تمام شود زودتر لاگ می‌شود (۱۰۰ اول تمام می‌شود)، ولی آرایه‌ی نتیجه همیشه به ترتیب همان آرایه‌ی ورودی است (۳۰۰، ۱۰۰، ۲۰۰)، نه به ترتیب اتمام.

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

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

نظرها

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

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

مطالب مرتبط

از همین دسته
آموزش

چرا Rust به‌جای segfault، خطای کامپایل می‌دهد

در C یا ++C یک اشاره‌گر نامعتبر معمولاً ماه‌ها بعد، در پروداکشن، خودش را نشان می‌دهد. در Rust همان اشتباه، همان لحظه‌ی کامپایل متوقفت می‌کند.

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