دليل المبتدئين لفهم Event Loop في NodeJS والكود المتزامن وغير المتزامن

دقائق القراءة: 6

مقدمة إلى NodeJS وبيئة التنفيذ غير المتزامنة

تُعد NodeJS بيئة تشغيل JavaScript مبنية لتنفيذ التطبيقات الشبكية القابلة للتوسع بكفاءة عالية. وتعتمد هذه البيئة على نموذج event-driven وآلية non-blocking I/O، ما يجعلها مناسبة جداً للخوادم والتطبيقات التي تتعامل مع عدد كبير من الطلبات في الوقت نفسه.

عندما نقول إن NodeJS تعمل بأسلوب asynchronous، فالمقصود أن بعض العمليات لا تُنفَّذ بشكل يحجب بقية الأوامر، بل تُعالَج في الخلفية إلى أن يحين وقت إتمامها. وهذا ما يمنح التطبيقات سرعة واستجابة أفضل مقارنة بالنماذج التقليدية التي تنتظر انتهاء كل عملية قبل بدء التالية.

توضيح بصري لمفهوم Event Loop في NodeJS وآلية تنفيذ العمليات المتزامنة وغير المتزامنة

في هذا الدليل، ستفهم كيف تتعامل NodeJS مع الكود المتزامن وغير المتزامن، وكيف تدير آلية Event Loop تنفيذ الأوامر داخل التطبيق.

ما هي Event Loop في NodeJS؟

Event Loop هي الآلية المسؤولة عن مراقبة الأحداث وإدارة تنفيذ callbacks داخل بيئة NodeJS. ويمكن تبسيطها على أنها حلقة تعمل باستمرار، تراقب ما إذا كانت هناك مهام جاهزة للتنفيذ، ثم تنقلها إلى Call Stack عندما يصبح ذلك ممكناً.

الحدث Event قد يكون أي شيء تقريباً، مثل:

  • نقرة من المستخدم
  • ضغط مفتاح
  • انتهاء مؤقت زمني
  • اكتمال قراءة ملف
  • استجابة من قاعدة بيانات أو API

أهمية Event Loop أنها تسمح لـ NodeJS بتنفيذ العمليات بكفاءة دون الحاجة إلى إنشاء خيط تنفيذ جديد لكل طلب.

الفرق بين Synchronous و Asynchronous programming

ما هو الكود المتزامن Synchronous؟

في البرمجة المتزامنة، يتم تنفيذ الأوامر بنفس الترتيب الذي كُتبت به. أي إن كل سطر ينتظر انتهاء السطر السابق قبل أن يبدأ.

إذا استدعيت دالة في برنامج متزامن، فلن ينتقل التنفيذ إلى السطر التالي حتى تنتهي هذه الدالة وتُعيد نتيجتها أو تنهي عملها.

انظر إلى المثال التالي:

const listItems = function (items) {
  items.forEach(function (item) {
    console.log(item)
  })
}

const items = ["Buy milk", "Buy coffee"]
listItems(items)

الناتج سيكون كالتالي:

Buy milk
Buy coffee

في هذا المثال، تُنفَّذ الدالة listItems(items) بشكل تسلسلي. تبدأ الحلقة بعرض العنصر الأول، ثم بعد اكتماله يتم عرض العنصر الثاني. لا يوجد أي انتقال إلى خطوة لاحقة قبل إنهاء الخطوة الحالية.

ما هو الكود غير المتزامن Asynchronous؟

أما في البرمجة غير المتزامنة، فلا تُنفَّذ جميع الأوامر بالتتابع الحرفي نفسه. توجد عمليات تُسجَّل ليتم تنفيذها لاحقاً عند تحقق شرط معين، مثل انتهاء مدة زمنية أو وصول استجابة من خدمة خارجية.

من أشهر الأمثلة على ذلك الدالة setTimeout():

setTimeout(function () {
  return console.log("Hello World!")
}, 3000)

هنا لن تُنفَّذ الدالة الداخلية فوراً، بل بعد مرور 3000 ميلي ثانية. والأهم من ذلك أن NodeJS لا تتوقف عن تنفيذ بقية الأوامر أثناء الانتظار، بل تواصل معالجة الكود المتزامن أولاً.

بمعنى آخر، تُعالَج المهام غير المتزامنة في الخلفية، ثم تُنفَّذ callbacks الخاصة بها عندما تسمح دورة التنفيذ بذلك.

قاعدة مهمة لفهم سلوك NodeJS

من الضروري فهم القاعدة التالية: العمليات غير المتزامنة لا تُنفَّذ داخل Call Stack مباشرة ما دام هناك كود متزامن لم ينتهِ بعد. فهي تنتظر حتى يصبح Call Stack فارغاً، ثم تتدخل Event Loop لنقل المهام الجاهزة إلى التنفيذ.

هذه الفكرة هي أساس السلوك غير الحاجب non-blocking الذي تشتهر به NodeJS.

كيف تعمل Event Loop داخل NodeJS؟

لفهم الصورة كاملة، من المهم التعرف إلى المكونات الأساسية داخل بيئة NodeJS:

  • Call Stack: المكان الذي تُنفَّذ فيه الدوال الحالية.
  • Node APIs: الجهة التي تتعامل مع العمليات غير المتزامنة مثل المؤقتات وعمليات الإدخال والإخراج.
  • Callback Queue: طابور تنتظر فيه الدوال الجاهزة للتنفيذ.
  • Event Loop: الحلقة التي تراقب Call Stack وCallback Queue وتنقل المهام عند توفر الفرصة.

مخطط يوضح مكونات بيئة NodeJS مثل Call Stack و Node APIs و Callback Queue و Event Loop

تنفيذ برنامج متزامن داخل NodeJS

في حالة الكود المتزامن، يكون التركيز بالكامل تقريباً على Call Stack، لأن جميع الأوامر تُنفَّذ مباشرة حسب ترتيبها.

Call Stack هو بنية بيانات من نوع stack، أي أن آخر دالة تدخل هي أول دالة تخرج. وتُستخدم هذه البنية لتتبع الدوال التي يجري تنفيذها حالياً.

عند بدء تنفيذ البرنامج، تقوم NodeJS بإحاطته ضمن دالة داخلية يمكن تصورها كأنها main(). ثم تُدفَع هذه الدالة إلى Call Stack.

إدخال الدالة الرئيسية main إلى Call Stack عند بدء تنفيذ برنامج متزامن في NodeJS

بعد ذلك، تُنشأ المتغيرات وتُخزن قيمها في الذاكرة. وعندما تصل عملية التنفيذ إلى دالة مثل console.log()، يتم دفعها إلى Call Stack وتنفيذها ثم إزالتها بعد الانتهاء.

تنفيذ console.log داخل Call Stack في NodeJS أثناء معالجة كود متزامن

بمجرد اكتمال جميع الأوامر، تُزال الدالة main() أيضاً، ويصبح Call Stack فارغاً.

إزالة الدوال من Call Stack بعد انتهاء التنفيذ المتزامن في NodeJSاكتمال تنفيذ البرنامج المتزامن في NodeJS مع تفريغ Call Stack بالكامل

كيف تنفذ NodeJS الكود غير المتزامن؟

لفهم تنفيذ العمليات غير المتزامنة، نحتاج إلى النظر إلى المكونات الثلاثة معاً: Call Stack وNode APIs وCallback Queue، بالإضافة إلى دور Event Loop.

لنتأمل السيناريو المعتاد الذي يحتوي على رسائل متزامنة ومؤقتات زمنية غير متزامنة.

مثال يوضح تنفيذ دوال متزامنة وغير متزامنة داخل NodeJS باستخدام setTimeout

الخطوة الأولى: بدء التنفيذ

كما يحدث دائماً، تبدأ main() بالدخول إلى Call Stack. ثم تُستدعى الدالة console.log("Start")، فتُنفَّذ مباشرة ويظهر الناتج في الطرفية، ثم تُزال من المكدس.

تنفيذ الرسالة Start داخل Call Stack عند بداية البرنامج في NodeJSظهور Start في المخرجات بعد تنفيذ console.log داخل NodeJS

الخطوة الثانية: التعامل مع setTimeout

عندما تصل NodeJS إلى setTimeout(...)، تُدفَع الدالة أولاً إلى Call Stack. لكن بما أنها عملية غير متزامنة، فلا يجري تنفيذ callback الخاص بها مباشرة داخل المكدس. بدلاً من ذلك، تُمرَّر إلى Node APIs ليُسجَّل المؤقت ويُتابَع في الخلفية.

نقل setTimeout من Call Stack إلى Node APIs لمعالجة المؤقت في الخلفيةتسجيل callback الخاصة بـ setTimeout داخل Node APIs في NodeJS

إذا وُجد setTimeout آخر بمدة مختلفة، فسيُسجَّل بالطريقة نفسها داخل Node APIs. خلال هذه الفترة، لا يتوقف البرنامج عن العمل، بل يستمر في تنفيذ ما تبقى من الكود المتزامن.

معالجة أكثر من مؤقت setTimeout داخل Node APIs دون حجب بقية تنفيذ البرنامجاستمرار تنفيذ الكود المتزامن أثناء انتظار انتهاء المؤقتات في NodeJS

هذا ما يُعرف بالسلوك non-blocking، حيث لا تمنع العمليات غير المتزامنة بقية الأوامر من التنفيذ.

الخطوة الثالثة: إكمال الكود المتزامن

بعد ذلك تُنفَّذ الدالة console.log("End") داخل Call Stack كآخر أمر متزامن. يظهر النص في المخرجات، ثم تُزال الدالة من المكدس. وعندما لا يبقى شيء آخر، تُزال main() أيضاً.

تنفيذ الرسالة End بعد انتهاء الأوامر المتزامنة في NodeJSتفريغ Call Stack بعد اكتمال تنفيذ main في مثال NodeJS غير المتزامن

الخطوة الرابعة: الانتقال إلى Callback Queue

في الخلفية، تكون المؤقتات قد انتهت أو بعضها قد انتهى. عند جاهزية callback الخاصة بأي عملية غير متزامنة، تُنقل إلى Callback Queue. يتم ترتيب هذه الدوال حسب وقت جاهزيتها.

انتقال callbacks الجاهزة من Node APIs إلى Callback Queue في NodeJS

هنا يأتي دور Event Loop. فهي تتحقق باستمرار مما إذا كان Call Stack فارغاً. وعندما يصبح فارغاً، تنقل أول callback من Callback Queue إلى Call Stack لتنفيذها.

الخطوة الخامسة: تنفيذ callbacks المؤجلة

بعد إفراغ Call Stack، تبدأ الدوال غير المتزامنة في الدخول واحداً تلو الآخر إلى المكدس عبر Event Loop، ثم تُنفَّذ وتُزال بعد اكتمالها.

نقل callback من Callback Queue إلى Call Stack عبر Event Loop لتنفيذها في NodeJSانتهاء تنفيذ جميع callbacks وتفريغ Call Stack و Callback Queue في NodeJS

لذلك قد تلاحظ أن دالة بمدة 0 ميلي ثانية لا تُنفَّذ فوراً قبل كل شيء، بل تنتظر حتى ينتهي الكود المتزامن أولاً. وهذا يفسّر لماذا قد تظهر End قبل ناتج setTimeout(..., 0).

ملخص تدفّق التنفيذ في NodeJS

المرحلة ما الذي يحدث؟
1 يدخل البرنامج إلى Call Stack عبر main().
2 تُنفَّذ الأوامر المتزامنة مباشرة حسب ترتيبها.
3 تُرسل العمليات غير المتزامنة إلى Node APIs.
4 عند الجاهزية، تنتقل callbacks إلى Callback Queue.
5 تراقب Event Loop حالة Call Stack.
6 عندما يفرغ المكدس، تُنقل callbacks إلى Call Stack للتنفيذ.

لماذا تعد Event Loop مفهوماً أساسياً لكل مطور NodeJS؟

فهم Event Loop ليس أمراً نظرياً فقط، بل ينعكس مباشرة على جودة كتابة الكود وتحسين الأداء. عندما تدرك كيف تُنفَّذ المهام، ستتمكن من:

  • تفسير ترتيب ظهور النتائج في الطرفية بدقة
  • تجنّب الأخطاء الناتجة عن سوء فهم التزامن
  • كتابة تطبيقات أسرع وأكثر قابلية للتوسع
  • تحسين استخدام callbacks وPromises وasync/await
  • فهم سبب عدم حجب المؤقتات وعمليات I/O لبقية التطبيق

نقاط عملية مهمة للمبتدئين

  1. setTimeout(…, 0) لا يعني التنفيذ الفوري، بل التنفيذ عند أول فرصة بعد انتهاء الكود المتزامن.
  2. وجود عملية غير متزامنة لا يعني أنها تعمل داخل Call Stack فوراً.
  3. Call Stack يجب أن يصبح فارغاً قبل تنفيذ أي callback مؤجلة.
  4. قوة NodeJS الحقيقية تظهر في التطبيقات التي تتعامل مع الشبكات والملفات والطلبات المتعددة.

الخلاصة التقنية

تعتمد NodeJS على نموذج ذكي يجمع بين Call Stack وNode APIs وCallback Queue تحت إشراف Event Loop. هذا النموذج هو السبب في قدرتها على تنفيذ العمليات غير المتزامنة دون تعطيل التدفق العام للتطبيق. ومن الناحية التقنية، فإن استيعاب هذا المفهوم مبكراً يمنح المطور أساساً قوياً لفهم الأداء، وتصميم الخدمات الخلفية، وكتابة كود أكثر كفاءة واعتمادية في مشاريع JavaScript الحديثة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *