JavaScript ایونٹ لوپ جو آپ کے ویب پیج کو چلاتا ہے، اس کا برتاؤ اس ایونٹ لوپ سے بہت مختلف ہے جو Node.js سرور کو چلاتا ہے، اور اگر آپ محتاط نہ ہوں تو یہ فرق UI کو منجمد (freeze) کر سکتا ہے یا I/O کو روک سکتا ہے۔ جو کوئی بھی ایسا async کوڈ لکھتا ہے جو دونوں ماحول میں چلتا ہے، اس کے لیے یہ جاننا ضروری ہے کہ یہ دونوں کہاں سے الگ ہوتے ہیں۔
یہ فرق کیوں اہم ہے
ایونٹ لوپ ECMAScript spec کے ذریعے بیان نہیں کیا جاتا؛ یہ ہوسٹ (host) میں ہوتا ہے۔ براؤزرز کو فریمز رینڈر کرتے وقت پیج کو ریسپونسو رکھنا پڑتا ہے، جبکہ Node.js نان بلاکنگ I/O کے گرد بنایا گیا ہے۔ ایک ہوسٹ میں کام کرنے والے پیٹرنز کو دوسرے میں استعمال کرنے سے ایسے بگ پیدا ہو سکتے ہیں جنہیں دوبارہ پیدا کرنا مشکل ہو: وعدوں (promises) کی ایک لمبی زنجیر براؤزر کے ری پینٹ (repaint) کو روک سکتی ہے، جبکہ ایک غیر چیک شدہ process.nextTick لوپ Node کو اس کے I/O مراحل تک پہنچنے سے روک سکتا ہے۔
براؤزر کا ٹرن بیسڈ لوپ
براؤزر میں، لوپ ایک واحد سائیکل چلاتا ہے جس میں ٹاسک کا نفاذ، مائیکرو ٹاسک کو مکمل کرنا، اور رینڈرنگ شامل ہوتی ہے:
- ایک میکرو ٹاسک چلائیں (جیسے کلک ہینڈلر،
setTimeoutوغیرہ)۔ - تمام مائیکرو ٹاسک ختم کریں (promises،
queueMicrotask)۔ - اگر کوئی فریم آنا ہے، تو 60 fps کے ہدف کو حاصل کرنے کے لیے پینٹ اور کمپوز کریں۔
- اسے دہرائیں۔
دو APIs ڈویلپرز کو اس سائیکل میں مداخلت کا موقع دیتی ہیں:
requestAnimationFrame– براؤزر کے پینٹ کرنے سے بالکل پہلے کال کیا جاتا ہے۔ یہ اینیمیشن کے کام کے لیے صحیح جگہ ہے کیونکہ کال بیک موجودہ مائیکرو ٹاسک کے بعد لیکن اگلے فریم سے پہلے چلتی ہے۔requestIdleCallback– اس وقت کال کیا جاتا ہے جب براؤزر کے پاس کوئی ہائی پرائیورٹی کام نہ ہو۔ یہ کم اثر والے کاموں جیسے اینالیٹکس یا ڈیٹا پری لوڈ کرنے کے لیے مفید ہے۔
خطرہ: مائیکرو ٹاسک اسٹارویشن (microtask starvation)
چونکہ براؤزر رینڈر کرنے سے پہلے مائیکرو ٹاسک کی قطار کو خالی کرتا ہے، اس لیے وعدوں (promises) کی ایک لمبی زنجیر UI کو کبھی پینٹ نہیں ہونے دیتی۔ کال اسٹیک بلاک نہیں ہوتی؛ پیج بس کبھی رینڈر مرحلے تک نہیں پہنچ پاتا، جو صارف کو منجمد ہونے (freeze) کا احساس دلاتا ہے۔
Node کا libuv-driven لوپ
Node.js اپنے لوپ کو libuv کے سپرد کرتا ہے، جو کہ ایک C لائبریری ہے جو کام کو مختلف مراحل میں تقسیم کرتی ہے، اور ہر مرحلے کی اپنی قطار ہوتی ہے:
- Timers –
setTimeoutاورsetIntervalسے کال بیکس۔ - Pending callbacks – تاخیر سے آنے والے I/O کال بیکس جو پہلے ہی OS لیول پر مکمل ہو چکے ہوں۔
- Poll – نئے I/O ایونٹس حاصل کرتا ہے (فائل ریڈز، نیٹ ورک ڈیٹا)۔
- Check –
setImmediateکال بیکس چلاتا ہے۔ - Close callbacks – جب کوئی ساکٹ یا ہینڈل بند ہوتا ہے تو چلتا ہے۔
دو ایسی چیزیں ہیں جو اس مرحلے کی ترتیب سے باہر ہیں:
process.nextTick– مائیکرو ٹاسک کی قطار سے پہلے، موجودہ آپریشن کے ختم ہوتے ہی چلتا ہے۔
خطرہ: I/O اسٹارویشن (I/O starvation)
اگر کوئی فنکشن بغیر رکے بار بار process.nextTick کو شیڈول کرتا ہے، تو Node کبھی "next-tick" مرحلے سے آگے نہیں بڑھ پاتا۔ نیٹ ورک ریکویسٹ، فائل ریڈز، اور ٹائمرز ساکت رہتے ہیں، جس سے سرور سائیڈ پر لیٹنسی (latency) میں اضافہ یا مکمل ہینگ ہونے کی صورتحال پیدا ہو سکتی ہے۔
عملی طور پر setImmediate بمقابلہ setTimeout
دونوں اگلے مرحلے کے لیے کال بیکس کو شیڈول کرتے ہیں، لیکن ان کی ترتیب اس بات پر منحصر ہے کہ انہیں کہاں کال کیا گیا ہے:
- ٹاپ لیول کوڈ – ترتیب کی ضمانت نہیں ہے؛ یہ اس بات پر منحصر ہے کہ پروسیس کتنی جلدی شروع ہوتا ہے۔
- I/O کال بیک کے اندر – ترتیب حتمی (deterministic) ہوتی ہے:
setImmediate،setTimeout(fn, 0)سے پہلے چلتا ہے۔ Poll مرحلہ ختم ہونے کے بعد، libuv Check مرحلے (جہاںsetImmediateہوتا ہے) پر جاتا ہے، اس سے پہلے کہ وہ زیرو ڈیلے ٹائماؤٹ کے لیے دوبارہ Timers مرحلے میں داخل ہو۔
یہ باریکی اس وقت اہم ہوتی ہے جب آپ درست ترتیب پر انحصار کرتے ہیں، جیسے کہ کسی ریڈ (read) کے مکمل ہونے کے فوراً بعد کسی ریسورس کو صاف کرنا۔
ایک نظر میں اہم فرق
- مقصد (Goal): براؤزرز بصری اپ ڈیٹس (visual updates) کو ترجیح دیتے ہیں؛ Node I/O کی تیاری کو ترجیح دیتا ہے۔
- رینڈرنگ ہک (Rendering hook):
requestAnimationFrame(صرف براؤزر)۔ - مرحلہ مخصوص ہک (Phase-specific hook):
setImmediate(صرف Node، Check مرحلے میں چلتا ہے)۔ - ہائی پرائیورٹی قطار (High-priority queue):
process.nextTick(صرف Node، مائیکرو ٹاسک سے پہلے چلتا ہے)۔ - اسٹارویشن کا خطرہ (Starvation risk): براؤزرز میں وعدوں (promises) کی لمبی زنجیریں؛ Node میں غیر محدود
process.nextTick۔
آگے کیا دیکھیں
اگر آپ ایسا کوڈ بیس برقرار رکھتے ہیں جو دونوں ماحول میں چلتا ہے (مثلاً isomorphic libraries)، تو ہر اس جگہ کا جائزہ لیں جہاں آپ:
- ایونٹ لوپ کو موقع دیے بغیر بہت سے وعدوں (promises) کو زنجیر وار (chain) کرتے ہیں۔
await new Promise(r => setTimeout(r, 0))شامل کریں یا براؤزر میںrequestIdleCallbackاستعمال کریں تاکہ رینڈرر کو موقع مل سکے۔ - ایسے کام کے لیے
process.nextTickاستعمال کرتے ہیں جسے مؤخر (defer) کیا جا سکتا ہے۔ جب آپ کو "next-tick" کی فوری ضرورت نہ ہو توsetImmediateیا ایک عام promise کو ترجیح دیں۔ - یہ فرض کرتے ہیں کہ
setTimeout(fn, 0)اورsetImmediateایک دوسرے کے متبادل ہیں۔ اگر ترتیب اہم ہے تو I/O کال بیکس کے اندر ان کی ترتیب کا تجربہ (test) کریں۔
خلاصہ
ایونٹ لوپ (event loop) ایک ہوسٹ کے مخصوص شیڈیولر (scheduler) ہے، نہ کہ JavaScript کی کوئی عالمگیر خصوصیت۔ براؤزرز رینڈرنگ (rendering) کو لوپ کے ساتھ جوڑتے ہیں؛ جبکہ Node، I/O کو libuv کے مراحل میں الگ رکھتا ہے۔ ترجیحی میکانزم کا غلط استعمال—جیسے براؤزر میں microtasks، یا Node میں process.nextTick—اس سسٹم کے اس حصے کو وسائل سے محروم کر سکتا ہے جس کی خدمت کے لیے ہر ماحول بنایا گیا ہے۔ اپنے async پیٹرنز کو ہوسٹ کے لوپ ماڈل کے مطابق رکھیں، اور آپ منجمد صفحات (frozen pages) اور بلاک شدہ سرورز (blocked servers) دونوں سے بچ سکیں گے۔
