المستوى: متوسط. المقال ده لأي حد كتب جافاسكريبت وبيستخدم setTimeout وPromise، بس لسه مش فاهم ليه الكود بيطلع بترتيب غير اللي كتبه. مش محتاج تكون خبير، بس محتاج تعرف الدوال والوعود (Promises).
لو setTimeout(fn, 0) عندك بيشتغل بعد كود كتبته تحته، ده مش باج ولا بطء في المتصفح. ده تصميم مقصود اسمه الـ Event Loop، ولما تفهمه هتوقف تخمين وتبقى تعرف بالظبط أي كود هيجري الأول.
الـ Event Loop في جافاسكريبت: ليه setTimeout بصفر بيشتغل في الآخر
المشكلة باختصار
جافاسكريبت لغة بخيط واحد (single-threaded). يعني في أي لحظة بتنفّذ سطر واحد بس. لكن المتصفح بيعمل حاجات كتير في نفس الوقت: بيحمّل صور، بيستنى ردود شبكة، بيسمع نقرات الفأرة. السؤال: إزاي لغة بخيط واحد بتتعامل مع كل ده من غير ما تتجمّد؟ الجواب هو الـ Event Loop. وسوء فهمه بيسبب أشهر أنواع الباجات: كود بيجري بترتيب غير متوقّع، وواجهة بتتجمّد فجأة.
مثال قبل النظرية: مطعم بطبّاخ واحد
تخيّل مطعم فيه طبّاخ واحد بس (ده الـ CPU thread بتاعك). قدّامه منضدة صغيرة بيحضّر عليها طلب واحد في المرة (ده الـ Call Stack). الطلبات الجاهزة للطبخ بتتحط في طابور على الشباك (ده Task Queue).
لكن في قاعدة صارمة في المطبخ ده: قبل ما الطبّاخ يبدأ أي طلب جديد من الطابور الكبير، بيبص الأول على "ورقة الملاحظات العاجلة" جنبه، وبينفّذ كل اللي فيها بالكامل. الورقة العاجلة دي هي الـ Microtask Queue. الوعود (Promises) بتكتب فيها. عشان كده الوعد بيتنفّذ قبل الـ setTimeout حتى لو الاتنين "جاهزين"، وحتى لو الـ timeout مكتوب فوقه في الكود.
دلوقتي المثال خلص، نرجع للتعريف العلمي الدقيق.
تفكيك المكوّنات علميًا
- Call Stack: مكدس بيشتغل بمبدأ LIFO (آخر داخل أول خارج). كل استدعاء دالة بيتحط فوق، ولما يخلّص بيتشال. طول ما فيه حاجة على المكدس، الـ Event Loop مبيعملش أي حاجة تانية.
- Web APIs: مش جزء من محرّك جافاسكريبت. دي قدرات بيوفّرها المتصفح (أو Node): المؤقّتات،
fetch، أحداث الـ DOM. لما تناديsetTimeout، المتصفح هو اللي بيعدّ الوقت، مش الـ thread بتاعك. - Macrotask Queue: طابور فيه المهام "الكبيرة": callbacks بتاعة
setTimeoutوsetInterval، أحداث الـ I/O، ورسم الواجهة. الـ Event Loop بياخد مهمة واحدة بس من هنا في كل لفّة. - Microtask Queue: طابور أعلى أولوية. فيه
Promise.then/catch/finally،await، وqueueMicrotask. القاعدة الذهبية: بعد كل مهمة كبيرة، الـ Event Loop بيفضّي طابور المهام الدقيقة بالكامل قبل ما ياخد المهمة الكبيرة اللي بعدها أو يرسم الشاشة.
الافتراض هنا: الكلام ده على سلوك المتصفحات الحديثة وNode.js حسب مواصفة HTML القياسية. Node عنده تفاصيل مراحل إضافية (مثل setImmediate وphases الـ libuv)، بس قاعدة "المهام الدقيقة الأول" ثابتة في الاتنين.
الكود اللي بيوضّح الترتيب
الكود ده تقدر تلزقه في console المتصفح دلوقتي وتجرّبه:
console.log('1: sync start');
setTimeout(() => console.log('4: timeout'), 0);
Promise.resolve().then(() => console.log('3: microtask'));
console.log('2: sync end');ناس كتير بتتوقّع الترتيب 1، 4، 3، 2 لأنهم بيقروا من فوق لتحت. لكن المخرج الحقيقي هو:
1: sync start
2: sync end
3: microtask
4: timeoutاللي بيحصل فعلاً بالتفصيل: السطرين المتزامنين (1 و2) بيجروا فورًا على الـ Call Stack. الـ setTimeout بيسلّم الـ callback للمتصفح، والمتصفح يحطّه في الـ Macrotask Queue. الـ Promise.then بيحط الـ callback بتاعه في الـ Microtask Queue. أول ما المكدس يفضى، الـ Event Loop بيفضّي المهام الدقيقة الأول (رقم 3)، وبعدها بس ياخد المهمة الكبيرة (رقم 4). عشان كده الصفر في setTimeout(fn, 0) مش معناه "دلوقتي"، معناه "في أقرب لفّة كبيرة جاية بعد ما تخلص المهام الدقيقة".
سيناريو واقعي بالأرقام
تخيّل صفحة فيها زرار "احسب" بيعمل عملية تقيلة على 50 ألف صف، وعايز تعرض رسالة "بنحسب..." قبلها. لو كتبت الكود كده:
status.textContent = 'بنحسب...';
heavyCalc(rows); // بياخد ~1200ms على الـ CPU threadالمستخدم مش هيشوف رسالة "بنحسب..." أبدًا. ليه؟ لأن تحديث الـ DOM ورسم الشاشة مهمة بتستنى الـ Event Loop ياخد لفّة جديدة، لكن heavyCalc بيحجز الـ thread 1200 مللي ثانية متواصلة، فالرسم مبيحصلش غير بعد ما الحساب يخلّص، واللحظة دي الرسالة بتتغيّر على طول للنتيجة. القياس هنا: زمن تجمّد ظاهر حوالي 1.2 ثانية، والواجهة مبتردّش على أي نقرة خلاله. الحل: خلّي المتصفح ياخد لفّة يرسم فيها الأول:
status.textContent = 'بنحسب...';
setTimeout(() => heavyCalc(rows), 0); // اسمح بلفّة رسم واحدة قبل الحسابدلوقتي الرسالة بتظهر خلال ~16 مللي ثانية (إطار واحد)، وبعدها يبدأ الحساب. نفس التجمّد بيحصل، بس المستخدم بقى شايف إن الصفحة بتشتغل.
الـ trade-offs
الاعتماد على الـ Microtask Queue بيديك أولوية عالية وتنفيذ قبل الرسم، وده كويس لما تعمل تحديثات حالة متسلسلة. بتكسب: ترتيب متوقّع وسرعة. بتخسر: لو زوّدت مهام دقيقة كتير أو عملت واحدة بتولّد واحدة تانية بلا نهاية، هتمنع الرسم تمامًا وتجمّد الصفحة رغم إن مفيش كود "تقيل". الـ trade-off هنا: الأولوية العالية سلاح بحدّين. المهام الكبيرة (setTimeout) أبطأ في الأولوية، بس بتدّي المتصفح فرصة يتنفّس ويرسم.
متى لا تستخدم هذه الطريقة (وأخطاء شائعة)
متستخدمش setTimeout(fn, 0) كأداة "تسريع" — هو مش بيسرّع حاجة، هو بس بيأجّل لبعد اللفّة الحالية. ومتحاولش تعمل حلقة على مهام دقيقة تولّد بعضها عشان "تفضّل شغّالة"؛ ده بيعمل تجويع (starvation) للـ Event Loop وبيوقّف الرسم. لو محتاج شغل تقيل فعلًا من غير تجميد، الحل مش الـ timeout ولا الـ microtask، الحل Web Worker على thread منفصل. وكمان: متفترضش إن الترتيب بين مؤقّتين بنفس القيمة مضمون بدقّة المللي ثانية؛ دقّة المؤقّتات محدودة وممكن تتقيّد لـ 4 مللي ثانية بعد تداخلات كتير.
الخطوة التالية
افتح console في أي صفحة دلوقتي، والصق أول مثال (الأربع أسطر) وشوف الترتيب 1-2-3-4 بعينك. بعدها جرّب تضيف queueMicrotask(() => console.log('micro 2')) بعد الـ Promise وخمّن فين هيطلع قبل ما تشغّل. لو المخرج طلع زي ما توقّعت، يبقى فهمت الـ Event Loop فعلًا.
المصادر
- MDN Web Docs — The event loop (مرجع الـ Call Stack والطوابير).
- WHATWG HTML Standard — Event loops (المواصفة الرسمية لترتيب المهام والمهام الدقيقة).
- Jake Archibald — Tasks, microtasks, queues and schedules (شرح تفصيلي مع أمثلة مقاسة).
- Node.js Docs — The Node.js Event Loop (مراحل libuv و
setImmediateوprocess.nextTick). - MDN — Using Web Workers (البديل للشغل التقيل بدون تجميد).