فهم Event Loop في JavaScript

دليل عملي يشرح ترتيب تنفيذ الشيفرة والمهام الصغيرة والكبيرة، ولماذا لا يعني setTimeout أن الكود سيتنفذ فور انتهاء المدة.

أسامة زيدانآخر تحديث: ١٨ يوليو ٢٠٢٦12 دقيقة قراءة

المشكلة التي يحلها Event Loop

عندما تكتب JavaScript في المتصفح، تبدو اللغة كأنها تنفذ أشياء كثيرة معًا: تستقبل نقرة، وتنتظر طلب شبكة، وتشغل مؤقتًا، ثم تحدث الواجهة. لكن محرك JavaScript نفسه ينفذ تعليمة واحدة في كل لحظة على الخيط الرئيسي. السر ليس تنفيذ الشيفرة بالتوازي، بل تنظيم الوقت بين الشيفرة الجاهزة والأحداث التي انتهت خارج المحرك.

يحتفظ محرك اللغة بمكدس استدعاءات Call Stack. عند استدعاء دالة توضع إطارها أعلى المكدس، وعند انتهاء الدالة يزال الإطار. لا يستطيع Event Loop إدخال callback جديد بينما المكدس مشغول؛ ينتظر حتى يصبح فارغًا. لهذا يمكن لحلقة حسابية ثقيلة أن تجمد الأزرار والرسم حتى لو كانت أحداث النقر تصل إلى المتصفح.

واجهات المتصفح مثل المؤقتات وfetch ليست جزءًا من محرك JavaScript. يبدأ المحرك العملية، ويتولى المتصفح انتظارها، ثم يضع العمل المرتبط بها في طابور مناسب عندما يصبح جاهزًا. بعد ذلك يختار Event Loop ما يمكن نقله إلى المكدس.

المهام العادية والمهام الصغيرة

ليست كل الطوابير متساوية. callbacks الخاصة بالمؤقتات وأحداث الواجهة تصل عادةً إلى طابور المهام Tasks. أما callbacks الناتجة عن Promise.then وawait فتصل إلى طابور المهام الصغيرة Microtasks. بعد انتهاء المهمة الحالية وفراغ المكدس، يفرغ المحرك طابور microtasks قبل الانتقال إلى المهمة العادية التالية.

هذا الترتيب يفسر لماذا تظهر نتيجة Promise قبل نتيجة setTimeout بقيمة صفر. المؤقت لا ينفذ داخل الدالة الحالية؛ هو يطلب من المتصفح جدولة callback كعمل لاحق. وعندما تنتهي الدالة، تنفذ microtasks أولًا ثم ينتقل Event Loop إلى المؤقت الجاهز.

توقع الترتيب قبل تشغيل المثال، ثم تحقق في Console.
console.log("start");

setTimeout(() => console.log("timer"), 0);

Promise.resolve().then(() => console.log("promise"));

console.log("end");

// start
// end
// promise
// timer

ماذا يحدث مع async وawait؟

الدالة async تبدأ كأي دالة عادية. عند الوصول إلى await تتوقف متابعة هذا الجزء من الدالة ويعاد Promise إلى المستدعي. عندما تستقر القيمة المنتظرة، تضاف متابعة الدالة إلى طابور microtasks. هذا يعني أن await لا يحجز الخيط أثناء انتظار الشبكة، لكنه لا يجعل كل ما بعده متوازيًا تلقائيًا.

إذا كان لديك طلبان مستقلان وكتبت await للأول ثم await للثاني، فقد صنعت انتظارًا متسلسلًا بلا داعٍ. ابدأ العمليتين أولًا ثم استخدم Promise.all. أما إذا كان الطلب الثاني يعتمد على نتيجة الأول، فالتسلسل مقصود وصحيح.

  • استخدم Promise.all للعمليات المستقلة.
  • لا تنشئ سلسلة microtasks لا تنتهي؛ قد تؤخر الرسم والمهام العادية.
  • قسّم الحسابات الثقيلة أو انقلها إلى Web Worker عندما تؤثر في الواجهة.

طريقة تشخيص ترتيب غير متوقع

ابدأ برسم ثلاثة أماكن: المكدس، طابور microtasks، وطابور tasks. مر على الشيفرة سطرًا سطرًا واكتب أين يذهب كل callback. لا تعتمد على التخمين من زمن المؤقت وحده. بعد ذلك ضع سجلات قصيرة تتضمن اسم المرحلة ووقتًا تقريبيًا، ثم راقب Performance panel إذا كانت المشكلة تجمدًا في الواجهة.

الهدف من فهم Event Loop ليس حفظ مصطلحات، بل اتخاذ قرارات أفضل: متى تبدأ الطلبات بالتوازي، لماذا تتأخر نقرة، وكيف تمنع مهمة طويلة من حجب الرسم. عندما تستطيع توقع مخرجات المثال السابق وشرحها دون تشغيله، تكون قد فهمت الأساس.

المراجع ومزيد من القراءة

عن المؤلف

يراجع أسامة زيدان محتوى كود هاب بهدف تقديم شرح عربي واضح يربط المفاهيم البرمجية بالقرارات التي يواجهها المتعلم أثناء التطبيق.

صفحة المؤلف