Event Loop: موتور پشت‌صحنه‌ی رفتار ناهمزمان جاوااسکریپت

در درس ۲۰-۱ دیدید که کدهایی مثل setTimeout اجرای برنامه را متوقف نمی‌کنند، بلکه به‌نوعی به تعویق می‌افتند. اما جاوااسکریپت که تک‌نخی است، دقیقاً چگونه بین اجرای کد اصلی و این عملیات‌های به‌تعویق‌افتاده هماهنگی ایجاد می‌کند؟ پاسخ این پرسش، مکانیزمی به نام Event Loop است که همراه با Call Stack و صف‌های وظیفه، رفتار ناهمزمان جاوااسکریپت را ممکن می‌سازد.

Call Stack: جایی که کد واقعاً اجرا می‌شود

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

function first() {
  second();
  console.log("پایان first");
}

function second() {
  console.log("داخل second");
}

first();
// Call Stack: first -> second -> (second تمام می‌شود) -> first -> (first تمام می‌شود)

Web APIs و Callback Queue

وقتی تابعی مثل setTimeout یا fetch فراخوانی می‌شود، خود عملیات زمان‌بر توسط مرورگر (نه موتور جاوااسکریپت) در بخشی جداگانه به نام Web APIs مدیریت می‌شود. پس از پایان این عملیات، تابع callback مربوطه مستقیماً به Call Stack اضافه نمی‌شود؛ بلکه ابتدا وارد یک صف انتظار به نام Callback Queue می‌شود.

نقش Event Loop

Event Loop وظیفه‌ای بسیار ساده اما حیاتی دارد: به‌طور مداوم بررسی می‌کند که آیا Call Stack خالی است یا نه. تنها زمانی که Call Stack کاملاً خالی باشد، اولین وظیفه از Callback Queue برداشته شده و به Call Stack اضافه می‌شود:

مشاهده‌ی عملی نقش Event Loop
خروجی کنسول اینجا نمایش داده می‌شود...

حتی با تأخیر صفر ثانیه، callback داخل setTimeout بلافاصله اجرا نمی‌شود؛ چون تا زمانی که کدهای همزمان باقی‌مانده در Call Stack تمام نشوند، Event Loop اجازه‌ی ورود هیچ وظیفه‌ای از Callback Queue را نمی‌دهد.

تفاوت میان Microtask Queue و Macrotask Queue

نکته‌ی مهمی که در تحلیل دقیق ترتیب اجرا باید در نظر گرفت این است که همه‌ی وظایف در یک صف واحد قرار نمی‌گیرند. Promiseها (از طریق then، catch و finally) در صفی جداگانه به نام Microtask Queue قرار می‌گیرند که همیشه اولویت بالاتری نسبت به Callback Queue معمولی (که به آن Macrotask Queue نیز گفته می‌شود) دارد:

console.log("۱");

setTimeout(() => console.log("۲ - از Macrotask Queue"), 0);

Promise.resolve().then(() => console.log("۳ - از Microtask Queue"));

console.log("۴");

// ترتیب واقعی خروجی:
// ۱
// ۴
// ۳ - از Microtask Queue
// ۲ - از Macrotask Queue

دلیل این ترتیب این است که Event Loop پیش از برداشتن هر وظیفه از Macrotask Queue، ابتدا تمام وظایف موجود در Microtask Queue را کامل اجرا می‌کند. به همین دلیل، Promise با وجود آنکه هم‌زمان با setTimeout با تأخیر صفر نوشته شده، همیشه زودتر اجرا می‌شود.

جدول مقایسه‌ی Microtask و Macrotask

ویژگی Microtask Queue Macrotask Queue
منابع اصلی then/catch/finally مربوط به Promise setTimeout، setInterval
اولویت اجرا بالاتر پایین‌تر
زمان تخلیه‌ی کامل صف پیش از هر Macrotask پس از خالی‌شدن کامل Microtask Queue

نکات کلیدی

  • Call Stack ساختاری است که اجرای توابع در حال اجرا را پیگیری می‌کند و تا خالی نشود، هیچ وظیفه‌ی جدیدی وارد اجرا نمی‌شود.
  • عملیات‌های ناهمزمان مثل setTimeout و fetch در Web APIs مدیریت می‌شوند و callback آن‌ها در یک صف انتظار قرار می‌گیرد.
  • Event Loop فقط زمانی که Call Stack خالی است، وظیفه‌ای از صف را وارد اجرا می‌کند.
  • Microtask Queue (مربوط به Promise) همیشه اولویت بالاتری نسبت به Macrotask Queue (مربوط به setTimeout) دارد.

جمع‌بندی

Event Loop، در کنار Call Stack و صف‌های Microtask و Macrotask، توضیح می‌دهد که چرا جاوااسکریپت با وجود تک‌نخی بودن، می‌تواند رفتاری شبیه چندوظیفگی از خود نشان دهد. تحلیل دقیق ترتیب اجرای کد—به‌خصوص وقتی Promise و setTimeout هم‌زمان استفاده می‌شوند—بدون شناخت این مکانیزم تقریباً غیرممکن است. این مفهوم پایه‌ی درک عمیق‌تر رفتار async/await نیز محسوب می‌شود که در ادامه‌ی این فصل بررسی خواهد شد.