حلقه رویداد Node.js چطور کار میکنه؟
خلاصهٔ کاملتر
جاوااسکریپت روی یه call stack تکنخی اجرا میشه — ساختاری LIFO که توابع رو به ترتیب فراخوانی روی هم میچینه و وقتی هر تابع تموم شد، فریمش برداشته میشه. تا زمانی که این stack خالی نشه، هیچ callback ای نمیتونه اجرا بشه. این همون مفهوم «blocking» هست: وقتی یه تابع stack رو اشغال میکنه و نمیذاره چیز دیگهای اجرا بشه. مثلاً یه عملیات crypto سنگین میتونه پنج ثانیه stack رو قفل کنه و حتی تایمرهایی که موعدشون رسیده رو به تأخیر بندازه.
Node.js برای حل این مشکل از سه لایه کنار هم استفاده میکنه: V8 که جاوااسکریپت رو کامپایل و اجرا میکنه، libuv که یه کتابخونه C هست و حلقه رویداد و I/O غیرهمزمان رو پیادهسازی میکنه، و C++ bindings که پل ارتباطی بین این دو دنیاست. وقتی fs.readFile صدا میزنی، V8 اون رو به کد C++ میفرسته، libuv کار رو به thread pool میسپاره، و وقتی نتیجه آماده شد callback جاوااسکریپتیت رو روی stack قرار میده.
libuv برای عملیاتی که ذاتاً blocking هستن (مثل بیشتر filesystem calls و بعضی DNS lookups) یه thread pool داخلی داره که بهصورت پیشفرض ۴ نخ داره. این نخها در پسزمینه کار میکنن تا main thread بلاک نشه. اندازه این pool رو میشه با متغیر محیطی UV_THREADPOOL_SIZE تغییر داد.
حلقه رویداد شش فاز داره که هر چرخه کامل یه «تیک» نامیده میشه:
- Timers: callback های setTimeout و setInterval که موعدشون رسیده اجرا میشن. libuv از یه min-heap استفاده میکنه تا نزدیکترین تایمر رو در O(1) پیدا کنه.
- Pending Callbacks: callback های I/O که به تیک بعدی موکول شدن (مثل خطاهای TCP).
- Idle/Prepare: فقط برای کارهای داخلی libuv.
- Poll: مهمترین فاز — منتظر رویدادهای I/O میمونه و callback های شبکه، فایل و سوکت رو اجرا میکنه.
- Check: callback های setImmediate() اینجا اجرا میشن، بلافاصله بعد از Poll.
- Close Callbacks: رویدادهای بستهشدن مثل socket.destroy().
فاز Poll قلب ماجراست. اگه صف خالی باشه و setImmediate() ای در راه باشه، مستقیم میره به فاز Check. اگه هیچی نباشه، صبر میکنه تا یه رویداد I/O بیاد. همین رفتار هوشمند باعث میشه Node بیدلیل CPU مصرف نکنه.
setImmediate() یه کاربرد عملی مهم داره: جدا کردن وظایف ثانویه از وظایف اصلی. مثلاً در یه اپ سفارش غذا، بعد از ذخیره سفارش در دیتابیس (که در Poll اجرا میشه)، میشه اعلان به رستوران رو با setImmediate() زمانبندی کرد تا در فاز Check اجرا بشه — بدون اینکه تأیید کاربر منتظر بمونه:
database.saveOrder(orderDetails, (error, savedOrder) => {
if (error) return console.error("Failed to save order!", error);
console.log(`Order #${savedOrder.id} confirmed in the database.`);
setImmediate(() => {
notificationService.sendToRestaurant(savedOrder);
});
console.log(`Sending confirmation to user for order #${savedOrder.id}.`);
});علاوه بر این شش فاز، میکروتسکها (Promise callbacks و process.nextTick) اولویت بالاتری دارن و بین هر فاز اجرا میشن — نه داخل فازها. این یعنی ترتیب اجرای واقعی: stack خالی میشه ← process.nextTick ← Promise jobs ← فاز بعدی libuv.
نکات کلیدی:
- call stack تکنخیست و تا خالی نشه هیچ callback ای اجرا نمیشه
- libuv حلقه رویداد رو پیادهسازی میکنه، نه خود Node یا V8
- فاز Poll برای I/O و فاز Check برای setImmediate() هست
- تایمرها با min-heap مدیریت میشن — اما ایجاد/حذف زیاد تایمر هزینه O(log n) داره
- میکروتسکها (Promise و nextTick) بین فازها اجرا میشن، نه داخلشون
- thread pool پیشفرض ۴ نخ داره و با UV_THREADPOOL_SIZE قابل تنظیمه




