الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

الـ Event Loop في جافاسكريبت: ليه setTimeout بصفر بيشتغل في الآخر

متوسط6 أغسطس 20267 دقائق قراءة

المستوى: متوسط. المقال ده لأي حد كتب جافاسكريبت وبيستخدم setTimeout وPromise، بس لسه مش فاهم ليه الكود بيطلع بترتيب غير اللي كتبه. مش محتاج تكون خبير، بس محتاج تعرف الدوال والوعود (Promises).

لو setTimeout(fn, 0) عندك بيشتغل بعد كود كتبته تحته، ده مش باج ولا بطء في المتصفح. ده تصميم مقصود اسمه الـ Event Loop، ولما تفهمه هتوقف تخمين وتبقى تعرف بالظبط أي كود هيجري الأول.

الـ Event Loop في جافاسكريبت: ليه setTimeout بصفر بيشتغل في الآخر

مكدس الاستدعاء على اليسار، حلقة الأحداث في المنتصف، وطابور المهام الدقيقة فوق طابور المهام الكبيرة على اليمين The JavaScript Event Loop why setTimeout(fn, 0) still runs last CALL STACK LIFO main() render() calc() EVENT LOOP WEB APIs / TIMERS setTimeout · fetch DOM events MICROTASK QUEUE · drains first, fully Promise.then queueMicrotask MACROTASK QUEUE · one per loop turn setTimeout I/O UI render
مكوّنات الـ Event Loop: مكدس الاستدعاء، حلقة الأحداث، وطابورا المهام الدقيقة والكبيرة.

المشكلة باختصار

جافاسكريبت لغة بخيط واحد (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 المتصفح دلوقتي وتجرّبه:

JavaScript
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
ثلاث مراحل: الكود المتزامن أولاً، ثم المهام الدقيقة، ثم مهمة كبيرة واحدة Execution order, not source order same code, output comes out 1 - 2 - 3 - 4 console.log('1: sync start'); setTimeout(() => console.log('4: timeout'), 0); Promise.resolve().then(() => console.log('3: microtask')); console.log('2: sync end'); 1. SYNCHRONOUS runs top to bottom 1: sync start 2: sync end 2. MICROTASKS drain fully, before UI 3: microtask Promise.then 3. MACROTASK one, next loop turn 4: timeout setTimeout(...,0)
ترتيب التنفيذ: كل الكود المتزامن، ثم كل المهام الدقيقة، ثم مهمة كبيرة واحدة.

اللي بيحصل فعلاً بالتفصيل: السطرين المتزامنين (1 و2) بيجروا فورًا على الـ Call Stack. الـ setTimeout بيسلّم الـ callback للمتصفح، والمتصفح يحطّه في الـ Macrotask Queue. الـ Promise.then بيحط الـ callback بتاعه في الـ Microtask Queue. أول ما المكدس يفضى، الـ Event Loop بيفضّي المهام الدقيقة الأول (رقم 3)، وبعدها بس ياخد المهمة الكبيرة (رقم 4). عشان كده الصفر في setTimeout(fn, 0) مش معناه "دلوقتي"، معناه "في أقرب لفّة كبيرة جاية بعد ما تخلص المهام الدقيقة".

سيناريو واقعي بالأرقام

تخيّل صفحة فيها زرار "احسب" بيعمل عملية تقيلة على 50 ألف صف، وعايز تعرض رسالة "بنحسب..." قبلها. لو كتبت الكود كده:

JavaScript
status.textContent = 'بنحسب...';
heavyCalc(rows);  // بياخد ~1200ms على الـ CPU thread

المستخدم مش هيشوف رسالة "بنحسب..." أبدًا. ليه؟ لأن تحديث الـ DOM ورسم الشاشة مهمة بتستنى الـ Event Loop ياخد لفّة جديدة، لكن heavyCalc بيحجز الـ thread 1200 مللي ثانية متواصلة، فالرسم مبيحصلش غير بعد ما الحساب يخلّص، واللحظة دي الرسالة بتتغيّر على طول للنتيجة. القياس هنا: زمن تجمّد ظاهر حوالي 1.2 ثانية، والواجهة مبتردّش على أي نقرة خلاله. الحل: خلّي المتصفح ياخد لفّة يرسم فيها الأول:

JavaScript
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 (البديل للشغل التقيل بدون تجميد).

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة