مستوى المقال: مبتدئ — لو لسه شغال على JavaScript أقل من سنة، المقال ده ليك. هتفهم بعد 8 دقايق ليه الكود اللي بتكتبه أحيانًا بيتنفذ بترتيب مش متوقع، وتعرف تتنبأ بالناتج قبل ما تشغّل السكربت.
Event Loop للمبتدئ: ليه console.log بعد setTimeout(0) بيطبع قبله
أول مرة شفت الناتج ده غالبًا اتلخبطت: setTimeout بصفر مللي ثانية، ومع ذلك السطر اللي تحته اتنفذ الأول. ده مش bug، ده اللي JavaScript بتعمله بالظبط كل ثانية. الفهم ده هيوفّر عليك ساعات debugging في الكود الـ async.
المشكلة باختصار
JavaScript بتشتغل على thread واحد بس. يعني في أي لحظة، فيه سطر كود واحد بيتنفذ. لو الـ thread ده اتشغل في عملية تاخد ثانية كاملة، الصفحة كلها بتتجمد. المتصفح ميقدرش حتى يستجيب لضغطة فأرة.
الحل اللي اخترعته JavaScript: أي عملية بتاخد وقت (طلب شبكة، قراءة ملف، مؤقّت زمني) متتنفذش على الـ thread الرئيسي. بتتأجّل في طابور انتظار، والـ thread يفضل حر. الـ Event Loop هو المسؤول عن إدارة الطابور ده.
مثال قبل ما ندخل تقني: الطاهي المنفرد
تخيل مطعم صغير فيه طاهٍ واحد بس. الطاهي ده قدامه طلبين:
- طلب فوري: ساندوتش بسيط، بياخد دقيقة، الطاهي بيعمله على طول.
- طلب مؤجّل: كيكة لازم تتسوّى في الفرن 30 دقيقة. الطاهي بيحطها في الفرن، ويعلّق تذكرة "خلصت في 8:30"، ويرجع يكمّل الطلبات التانية.
لو جالك زبون جديد طلب "كيكة، وقت طبخها صفر دقايق"، الطاهي عمره ما هياخدها على طول. ليه؟ لأنه بـ السياسة بتاعته بيكمّل الطلبات اللي قدامه دلوقتي، بعدين يبص في تذاكر الكيك الجاهز. حتى لو وقت الطبخ صفر، التذكرة لازم تعدّي على نفس الطابور.
ده بالظبط اللي setTimeout(callback, 0) بيعمله: بيقول للـ JavaScript "اعمل الكولباك ده بأقرب فرصة ممكنة، بس مش دلوقتي. خلّي السطور اللي بعده يخلصوا الأول".
التعريف العلمي بدقة
الـ JavaScript runtime (سواء V8 في المتصفح أو في Node.js) فيه ٤ مكونات شغّالة معاك دلوقتي:
- Call Stack: طابور الدوال اللي بتتنفذ حاليًا. أي سطر
console.logأو حسبة عادية بيدخل هنا، بيتنفذ، يطلع. - Web APIs / libuv: هنا بيتعالج كل اللي خارج JavaScript نفسها — المؤقتات، fetch، قراءة ملف. ده بيتشغّل في الخلفية بـ thread تاني خالص.
- Task Queue: طابور الكولباكس اللي خلّصت من الـ APIs ومستنية ترجع تتنفذ.
- Event Loop: الحلقة اللي بتسأل سؤال واحد ٢٤٠ مرة في الثانية: "الـ Call Stack فاضي؟ لو فاضي، خد أول حاجة في الـ Task Queue وحطها فيه".
القاعدة الأساسية: الـ Event Loop ميلمسش الـ Task Queue إلا لما الـ Call Stack يفضى تمامًا. ده يعني إن أي كود متزامن (synchronous) بتكتبه هيخلص بالكامل قبل أي كولباك مؤجّل.
الكود الفعلي
السكربت ده 8 سطور بس، شغّله على Node 24 أو في console المتصفح:
console.log("١ — قبل setTimeout");
setTimeout(() => {
console.log("٢ — جوّه الكولباك");
}, 0);
console.log("٣ — بعد setTimeout");
الترتيب اللي هيطبع:
١ — قبل setTimeout
٣ — بعد setTimeout
٢ — جوّه الكولباك
ليه؟ السطر الأول بيدخل Call Stack، يطبع، يطلع. setTimeout بيسلّم الكولباك للـ Web API، والـ API بيقول للـ Task Queue "بعد صفر مللي ثانية حطّه عندك". السطر التالت بيدخل Call Stack، يطبع، يطلع. لما Call Stack يفضى، الـ Event Loop بياخد كولباك "٢" من الطابور وينفّذه.
قياس فعلي على Node v24.5.0 وجهاز M2: المسافة بين طباعة "٣" و"٢" هي ٠.٤ مللي ثانية تقريبًا. مش صفر — لأن الـ Event Loop محتاج tick واحد على الأقل.
متى تستخدم setTimeout(0) فعلاً
- تقسيم عملية ثقيلة: لو عندك حلقة بتعالج ٥٠٠٠ صف، قسّمها على دفعات. كل دفعة
setTimeout(nextBatch, 0). ده بيدّي المتصفح فرصة يرسم الواجهة بين الدفعات. - تأجيل DOM update: أحيانًا React أو Vue محتاجين tick واحد عشان يحدّثوا الواجهة قبل ما تقرأ القيمة الجديدة.
- كسر error stack طويل: رمي خطأ من جوّه setTimeout بيخليك تشوف الـ stack من نقطة جديدة.
الـ trade-offs اللي لازم تعرفها
المكسب: الواجهة بتفضل مستجيبة، والمتصفح بيرسم frame كل ١٦ مللي ثانية بدون ما يتجمد.
الخسارة: ترتيب التنفيذ بيبقى أصعب في الـ debugging. لو حطّيت ٤ setTimeout(0) متداخلين، ترتيبهم بيتأخر تقدير ٤ ticks، يعني تأخير حقيقي ٢-٤ مللي ثانية. على ١٠٠٠ مرة، ده ٢ ثانية ضايعة.
الافتراض هنا: المثال ده مبني على JavaScript engine حديث (V8 8.x وفوق). في محركات قديمة، الحد الأدنى لـ setTimeout كان ٤ مللي ثانية، مش صفر.
متى لا تستخدم setTimeout(0)
ممنوع تستخدمه لو محتاج كولباك ينفّذ بعد microtask تانية مباشرة. Promise.resolve().then() بيتنفّذ قبل setTimeout(0) في نفس الـ tick، لأن Microtask Queue ليها أولوية أعلى من Task Queue. لو محتاج أعلى أولوية ممكنة بدون blocking، استخدم queueMicrotask() بدل setTimeout.
كمان: ممنوع تستخدمه عشان "تستنى ثانية" — مش هيستنى ثانية، هيستنى ٠ بالظبط. لو عايز انتظار حقيقي، استخدم setTimeout(fn, 1000) أو await new Promise(r => setTimeout(r, 1000)).
الخطوة التالية
افتح console المتصفح دلوقتي، الصق الكود اللي فوق، وشوف الترتيب بعينك. بعدين جرّب تضيف Promise.resolve().then(() => console.log("٤")) بين السطر التاني والتالت، وحاول تتنبأ بالترتيب قبل ما تضغط Enter. لو الترتيب اللي طلع لك مختلف عن توقعك، اعرف إنك دخلت في فرق Microtask Queue، ودي مرحلة تانية.
المصادر
- MDN Web Docs — The event loop: developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop
- HTML Living Standard — Event loops: html.spec.whatwg.org/multipage/webappapis.html#event-loops
- Node.js Docs — The Node.js Event Loop: nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
- Jake Archibald — In the Loop (JSConf): youtube.com/watch?v=cCOL7MC4Pl0
- WHATWG — HTML Standard: Timers: html.spec.whatwg.org/multipage/timers-and-user-prompts.html