المستوى: مبتدئ — لو كتبت قبل كده setTimeout(fn, 0) وافتكرت إنها هتشتغل في نفس اللحظة، المقال ده هيوريك ليه بتتأخّر، وإزاي جافاسكريبت بتعمل مليون حاجة في نفس الوقت وهي أصلًا بتشتغل بخيط واحد بس.
الـ Event Loop في جافاسكريبت مش سحر، ده طابور منظّم
جافاسكريبت single-threaded، يعني بتنفّذ أمر واحد في اللحظة. ومع ذلك بتقدر تحمّل صورة، وتستنى رد من سيرفر، وتستجيب لكليك المستخدم، كل ده من غير ما الصفحة تتجمّد. اللي بيخلّي ده ممكن اسمه الـ Event Loop.
المشكلة باختصار
كتير من المبتدئين بيتصدموا لما يشوفوا كود زي ده يطبع نتيجة مش متوقّعة. الافتراض إن الأوامر بتتنفّذ من فوق لتحت بالترتيب. ده صح، لكن مش كل أمر بيتنفّذ في نفس اللحظة. بعض الأوامر بتتأجّل، والـ Event Loop هو اللي بيقرّر امتى ترجع.
الأول مثال بسيط: نادل واحد في مطعم
تخيّل مطعم فيه نادل واحد بس بيخدم خمس ترابيزات. لو النادل خد أوردر من ترابيزة، وبعدين وقف جنب المطبخ مستنّي الأكل يجهز قبل ما يروح لأي حد تاني، المطعم هيتشلّ. الأربع ترابيزات التانية هيقعدوا يستنّوا فاضيين.
النادل الشاطر بيعمل حاجة تانية. بياخد الأوردر، بيسلّمه للمطبخ، وبيروح فورًا لترابيزة تانية. لما المطبخ يخلّص، بيناديه، فيرجع يقدّم الأكل. النادل لسه واحد، بس مبيقفش يستنّى. ده بالظبط اللي بتعمله جافاسكريبت.
النادل هو الخيط الوحيد بتاع جافاسكريبت. المطبخ هو الأشياء اللي بتشتغل في الخلفية (زي الشبكة أو المؤقّتات). ونداء المطبخ للنادل لما الأكل يجهز، ده الـ callback اللي بيدخل الطابور.
دلوقتي الشرح العلمي الدقيق
في المتصفح فيه أربع قطع بتشتغل مع بعض:
- مكدّس الاستدعاء (Call Stack): المكان اللي بيتنفّذ فيه الكود سطر ورا سطر. حاجة واحدة بس فوق المكدّس في أي لحظة.
- واجهات المتصفح (Web APIs): حاجات المتصفح بيتكفّل بيها بره الخيط الرئيسي، زي
setTimeoutوfetch. - طابور المهام (Callback Queue): لما مهمة خلفية تخلّص، الـ callback بتاعها بيقف في طابور مستنّي دوره.
- حلقة الأحداث (Event Loop): بتراقب المكدّس. لحظة ما المكدّس يفضى، بتسحب أول callback من الطابور وتحطّه في المكدّس ليتنفّذ.
القاعدة الذهبية: الـ Event Loop مبتحطّش حاجة جديدة في المكدّس إلا لما المكدّس يبقى فاضي تمامًا. عشان كده أي callback بيستنّى لغاية ما الكود الأساسي كله يخلّص.
الكود اللي بيوريك ده بعينك
جرّب الكود ده في الـ console بتاع أي متصفح:
console.log("1");
setTimeout(function () {
console.log("2");
}, 0);
console.log("3");
// الناتج الفعلي:
// 1
// 3
// 2
رغم إن التأخير صفر ثانية، الرقم 2 طلع في الآخر. ليه؟ لإن setTimeout بيسلّم الـ callback لواجهة المتصفح. حتى لو المؤقّت خلص فورًا، الـ callback مبيدخلش المكدّس على طول، بيقف في الطابور. والـ Event Loop مش هتسحبه غير لما الكود الأساسي (سطر 1 وسطر 3) يخلّص. يعني الصفر ده معناه "في أقرب فرصة بعد ما المكدّس يفضى"، مش "دلوقتي".
مستوى أعمق: مش كل طابور واحد
في الحقيقة فيه طابورين. المهام الصغيرة (microtasks) زي الـ Promises، والمهام الكبيرة (macrotasks) زي setTimeout. الـ Event Loop بتفضّي طابور المهام الصغيرة بالكامل قبل ما تلمس الكبيرة. شوف:
console.log("start");
setTimeout(function () { console.log("timeout"); }, 0);
Promise.resolve().then(function () { console.log("promise"); });
console.log("end");
// الناتج:
// start
// end
// promise (المهمة الصغيرة اتنفّذت الأول)
// timeout (المهمة الكبيرة استنّت)
الافتراض هنا إنك بتشتغل في بيئة متصفح حديثة أو Node.js. الترتيب ده جزء من مواصفة HTML الرسمية، مش سلوك عشوائي بيختلف من متصفح للتاني.
ليه ده مهم فعلًا: سيناريو واقعي وأرقام
افترض عندك صفحة فيها زرار، ولمّا المستخدم يدوس بتعمل عملية حسابية تقيلة تاخد 3 ثواني بشكل متزامن (synchronous). خلال الـ 3 ثواني دول، الصفحة بتتجمّد بالكامل: مفيش كليك بيشتغل، ومفيش scroll، وحتى مؤشّر التحميل بيقف. السبب إن المكدّس مشغول، والـ Event Loop مقفولة.
الشاشة العادية بتتحدّث 60 مرة في الثانية، يعني عندك حوالي 16 مللي ثانية لكل إطار. أي مهمة متزامنة بتاخد أكتر من 50 مللي ثانية بيحسّها المستخدم كـ "تهنيج". لو موقعك بيخدم 50 ألف زائر في اليوم، تجميد نص ثانية في كل تفاعل بيتحوّل لآلاف اللحظات المزعجة يوميًا. الحل: تقسّم الشغل التقيل، أو تنقله لـ Web Worker يشتغل على خيط تاني بعيد عن الـ Event Loop.
الـ trade-offs اللي لازم تعرفها
النموذج ده مبيجيش ببلاش. بتكسب واجهة مستخدم بتفضل حيّة ومتفاعلة حتى وإنت بتستنّى الشبكة. بتخسر بساطة القراءة السطرية للكود؛ لإن ترتيب التنفيذ مبقاش من فوق لتحت بشكل مباشر، وده بيصعّب تتبّع الأخطاء على المبتدئ. القاعدة العملية: استخدم async/await بدل الـ callbacks المتداخلة عشان ترجّع للكود شكله السطري وإنت محافظ على عدم التجميد.
متى لا تشغّل بالك بالـ Event Loop
لو بتكتب سكربت صغير بيتنفّذ مرة وينتهي (زي حساب بسيط أو تحويل بيانات)، مفيش داعي تفكّر في الطوابير خالص. كمان لو مشكلتك حسابية بحتة وبتستهلك المعالج (CPU-bound) زي معالجة صورة ضخمة، الـ Event Loop مش هتنقذك؛ لإن المشكلة مش في الانتظار، المشكلة في الشغل نفسه اللي بيقفل الخيط. هنا محتاج Web Workers في المتصفح أو worker threads في Node.js، مش فهم أعمق للطابور.
الخطوة التالية
افتح الـ console دلوقتي، والصق مثال الـ Promise والـ setTimeout فوق، وقبل ما تدوس Enter اكتب على ورقة الترتيب اللي متوقّعه. لو النتيجة طلعت غير توقّعك، إنت لسه فاهم الطابورين غلط، وده بالظبط المكان اللي المفروض تركّز فيه.
المصادر
- MDN Web Docs — "The event loop" (تعريف المكدّس والطابور وحلقة الأحداث): developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
- WHATWG HTML Living Standard — "Event loops" (المواصفة الرسمية لترتيب المهام الصغيرة والكبيرة): html.spec.whatwg.org
- Node.js Docs — "The Node.js Event Loop, Timers, and process.nextTick()": nodejs.org
- Jake Archibald — "In The Loop" (JSConf.Asia): شرح مرئي لترتيب المهام الصغيرة والكبيرة.
- web.dev — "Rendering performance": معدّل تحديث الشاشة 60Hz وحدود الـ 16ms لكل إطار.