2.2 · باعث الأحداث (EventEmitter) والبرمجة الحدثية

المستوى 2 · قلب Node.js

2.2باعث الأحداث (EventEmitter) والبرمجة الحدثية

الهدف: فهم نموذج Node الحدثي واستخدامه (الإصدار/الاستماع عبر باعث الأحداث (EventEmitter) و.on/.emit)، وهو محوري للتعامل مع واجهات Node.
الوقت المقدّر: 10 دقيقة · قراءة ذاتية · إذا كان لديك 5 دقائق فقط: اقرأ «الأساسي في 30 ثانية» + المخطط.

الأساسي في 30 ثانية

  • أصدِر ثم استمع: يعلن .on (التسجيل) التفاعل، ويستدعيه .emit (الإطلاق) — والطرفان لا يعرف أحدهما الآخر.
  • في كل مكان تحت الغطاء: خادم HTTP والتدفقات والمقابس كلها باعثات أحداث — فمعرفة الاستماع هي معرفة استخدام Node.
  • الحدث error لا يرحم: حدث error مُصدَر دون مستمع يُسقِط البرنامج — استمع للأخطاء دائمًا.

حالة مستمرة — ساليم يشير إلى تقدّم الاستيراد

يطوّر ساليم جرده (2.1): يستغرق استيراد التقارير وقتًا، ويريد الفريق تتبّع التقدّم دون استدعاء سكربته في حلقة. ينشئ مُصدِرًا ينشر حدثًا لكل ملف معالَج وحدث نهاية — فيستمع عارض التقدّم والسجل باستقلالية، دون أن يعرفهما الاستيراد. وفي 2.3، ستُصدِر التدفقات نفسها أحداث data وend، وفي 2.4 سيُصدِر خادم HTTP طلبًا عند كل زيارة.

تتمة الخيط الرابط 2.1: يصبح جرد ساليم مفكوكًا بالأحداث — ويستمر في 2.3 و2.4.

1. إصدار الأحداث والاستماع والتفاعل

إن Node في جوهره حدثي (تذكير من المستوى 0: حلقة الأحداث). تقوم البرمجة الحدثية على فكرة بسيطة: بدل أن يستدعي الكود دوال أخرى مباشرة، تُصدِر كائنات أحداثًا («حدث شيء ما») وتستمع إليها أجزاء أخرى من الكود وتتفاعل معها. وهي آلية فكّ ارتباط: فالمُصدِر لا يعرف من يستمع إليه، والمستمع لا يعرف مصدر الحدث — ويتواصلان عبر اسم الحدث. ولم يعد استيراد ساليم بحاجة لمعرفة العارض: إنه يُصدِر، والعارض يستمع.

2. الدالتان .on و.emit، مع once وoff

يقوم العمل على طريقتين. .on(الاسم، ردّ النداء): تسجيل مستمع — «عند إصدار X، نفّذ هذه الدالة». .emit(الاسم، ...البيانات): الإصدار — «أطلِق X بهذه البيانات»، فيُستدعى حينها كل مستمعيه. ننشئ المُصدِر الخاص بنا بـnew EventEmitter() من الوحدة الأصلية events (أو بجعل صنف يرث منه). ومفيد أيضًا: .once() (الاستماع مرة واحدة فقط، مع إلغاء اشتراك تلقائي) و.off() / .removeListener() (إيقاف الاستماع). والقاعدة الذهبية: الحدث error خاص — فإصداره دون مستمع يُسقِط البرنامج. لذلك يستمع ساليم دائمًا لأخطاء استيراده.

3. النموذج تحت كل واجهات Node

لماذا هذا مهم: لأنه النموذج الكامن وراء عديد من الواجهات — خوادم HTTP (حدث عند كل طلب)، والتدفقات ('data' و'end')، والمقابس، وكثير من المكتبات تعرض سلوكياتها عبر الأحداث. فمعرفة الاستماع (.on('data', ...) و.on('request', ...)) ثابتة في Node. ويتيح باعث الأحداث (EventEmitter) أيضًا بنية مفكوكة وقابلة للتوسّع: فإضافة سلوك = إضافة مستمع، دون المساس بالمُصدِر. ولن ننشئ بالضرورة باعثات مخصصة كثيرة في البداية، لكننا سنصادفها باستمرار — ومن هنا أهمية نموذج الإصدار/الاستماع.

خطأ شائع في الفهم: «الردود الندائية والوعود وباعث الأحداث شيء واحد — سأستخدم الوعود لكل شيء». خطأ. فالوعد يمثّل نتيجة وحيدة واحدة ويُحسَم مرة واحدة: مثالي لـ«اقرأ هذا الملف وأعطني محتواه». بينما ينشر باعث الأحداث (EventEmitter) أحداثًا قد تقع مرات عديدة (كل طلب لخادم، كل قطعة من تدفق) لصفر أو واحد أو عدة مستمعين مستقلين. فإرادة وعد لكل طلب مستلَم أو لكل قطعة بيانات هو استخدام الأداة الخطأ: التدفق المتكرر وفكّ الارتباط = باعث الأحداث.
أصدِر، استمع، تفاعل مفكوك بالاسم المُصدِر.emit(الاسم، البيانات): حدث شيء ما+اسم الحدثالرابط الوحيد بين الطرفين+المستمعون.on(الاسم، التفاعل): كل واحد يتفاعل بطريقته إضافة سلوك = إضافة مستمع، دون المساس بالمُصدِر.
مفكوك بالاسم
نص المخطط (SEO / قارئات الشاشة / الترجمة): 1) استيراد ساليم يُصدِر: حدثًا لكل ملف معالَج، ثم حدث نهاية — ولا يعرف أي متلقٍّ. 2) يصنع اسم الحدث الرابط: فيشترك فيه العارض والسجل بـ.on باستقلالية. 3) يتفاعل كل واحد بطريقته: شريط تقدّم من جهة، وسطر سجل من جهة أخرى. الدرس: الإصدار/الاستماع يفكّ الارتباط — وهذا النموذج هو نموذج الخوادم والتدفقات والمقابس الأصلية.

🔎 مثال ملموس — 🔎 مثال ملموس — تقدّم استيراد ساليم

مُصدِر واحد ومستمعان مستقلان، كما عند ساليم:

const { EventEmitter } = require('node:events');
const progression = new EventEmitter();

progression.on('fichier', (nom) => console.log('Traité :', nom));
progression.on('termine', (total) => console.log('Import fini :', total));
progression.on('error', (err) => console.error('Échec import :', err.message));

progression.emit('fichier', 'rapport-01.txt');
progression.emit('termine', 1);

القراءة: الاستيراد يُصدِر، والعرض والسجل يستمعان دون معرفة أحدهما الآخر.

الدرس: مستمع error إلزامي — وإلا حدث السقوط.

مفردات القسم

البرمجة الحدثية
نموذج تُصدِر فيه كائنات أحداثًا وتتفاعل معها أجزاء أخرى من الكود بالاستماع؛ وفكّ الارتباط عبر اسم الحدث.

مثال بسيط: استيراد ساليم يُصدِر، والتقدّم والسجل يستمعان.

ما ليس كذلك: استدعاء مباشر لدالة يعرف فيه المستدعي المتلقي.

رد فعل: تفاعلات مستقلة متعددة = التفكير في الأحداث.

باعث الأحداث (EventEmitter)
صنف أصلي (وحدة events) في قلب النموذج الحدثي لـNode؛ وترث منه عديد من الكائنات الأصلية (خادم HTTP والتدفقات).

مثال بسيط: ينشئ ساليم new EventEmitter() لتقدّم استيراده.

ما ليس كذلك: وعد، لا يُحسَم إلا مرة واحدة.

رد فعل: كائن Node يُصدِر = على الأرجح باعث أحداث.

.on() / .emit()
تسجّل on(الحدث، ردّ النداء) مستمعًا؛ وتطلق emit(الحدث، البيانات) الحدث وتستدعي كل مستمعيه.

مثال بسيط: ‏progression.on('fichier', ...) ثم ‏progression.emit('fichier', nom).

ما ليس كذلك: إرجاع دالة يعيد قيمة إلى المستدعي.

رد فعل: .on قبل .emit، وإلا ضاع الحدث في الفراغ.

.once() / .off()
تستمع once() لحدث مرة واحدة فقط (إلغاء اشتراك تلقائي)؛ وتوقف off()/removeListener() الاستماع.

مثال بسيط: يستمع ساليم لحدث نهاية الاستيراد بـ.once().

ما ليس كذلك: مستمع يبقى نشطًا للأبد افتراضيًا.

رد فعل: حدث وحيد منتظَر = .once().

حدث error
حدث خاص: مُصدِر يُصدِر error دون مستمع يُسقِط البرنامج — استمع للأخطاء دائمًا.

مثال بسيط: يضيف ساليم progression.on('error', ...) منذ الإنشاء.

ما ليس كذلك: حدث عادي يمكن تجاهله دون خطر.

رد فعل: مُصدِر جديد = مستمع error فوري.

في التطبيق — تطبيق عملي — استخدام البرمجة الحدثية

  1. أنشئ باعث أحداث (EventEmitter) ‏(const emitter = new EventEmitter())، وسجّل مستمعًا بـ.on('event', callback) وأصدِر بـ.emit('event', données).
  2. مرّر بيانات عند الإصدار واستعدها في المستمع؛ وأضف عدة مستمعين على الحدث نفسه.
  3. جرّب .once() (استماع وحيد) واستمع دائمًا لحدث 'error' (وإلا حدث السقوط).
  4. لاحظ أن كائنات Node الأصلية باعثات أحداث: استمع مثلًا لأحداث خادم HTTP أو تدفق (.on('data', ...)).
أصبحت تفهم نموذج Node الحدثي وتستخدمه (الإصدار/الاستماع عبر باعث الأحداث (EventEmitter) و.on/.emit)، وهو محوري للتعامل مع واجهات Node (الخوادم والتدفقات…).

نص جاهز — شرح فكّ الارتباط لزميل (30 ثانية)

«استيرادي لا يعرف العارض ولا السجل: إنه يُصدِر فقط حدثًا لكل ملف معالَج. ويشترك كل واحد بـ.on ويتفاعل بطريقته. فإذا أضفنا غدًا تنبيه بريد إلكتروني، نضيف مستمعًا — دون المساس بسطر واحد من الاستيراد.»

يُقال مع عرض إضافة مستمع مباشرة؛ فالبرهنة هي المقنعة.

نموذج نسخ ولصق — استيراد مُتابَع بالأحداث

const { EventEmitter } = require('node:events');

class Importeur extends EventEmitter {
  async importer(fichiers) {
    let traites = 0;
    for (const f of fichiers) {
      try {
        traites += 1;
        this.emit('fichier', f, traites, fichiers.length);
      } catch (err) {
        this.emit('error', err);
        return;
      }
    }
    this.emit('termine', traites);
  }
}

const importeur = new Importeur();
importeur.on('fichier', (f, n, total) => console.log(n + '/' + total + ' : ' + f));
importeur.once('termine', (total) => console.log('Import fini : ' + total + ' fichiers'));
importeur.on('error', (err) => console.error('Échec : ' + err.message));

importeur.importer(['rapport-01.txt', 'rapport-02.txt']);

سكربت منفرد

يكفي مستمع تقدّم واحد: يبقى المُصدِر متماثلًا، ونضيف مستمعين عندما تكبر الحاجة.

فريق في الجزائر

يضيف ساليم مستمعًا يسجّل كل ملف في ملف المتابعة المشترك — دون تعديل الاستيراد.

حدث وحيد

نهاية الاستيراد، انتهاء التهيئة: يتجنّب .once() التفاعلات المزدوجة إذا أُعيد إصدار الحدث.

كائن أصلي

بدل إنشاء مُصدِر، استمع للموجود: خادم HTTP أو تدفق أو process — ‏.on('data'/'request'/'exit').

✓ ممارسات جيدة

  • سجّل المستمعين (.on) قبل الإصدار، وإلا ضاع الحدث في الفراغ.
  • استمع دائمًا لـerror على كل مُصدِر (وإلا سقط البرنامج).
  • أضف سلوكًا بإضافة مستمع، لا بتعديل المُصدِر.

✗ أخطاء شائعة

  • استخدام وعد لتدفق متكرر (طلبات، قطع تدفق).
  • نسيان مستمع error (السقوط عند أول مشكلة).
  • الإصدار قبل تسجيل المستمعين (أحداث ضائعة).

النقاط الأساسية

  • البرمجة الحدثية (في قلب Node): كائنات تُصدِر أحداثًا، وأخرى تستمع إليها وتتفاعل — فكّ ارتباط عبر اسم الحدث.
  • باعث الأحداث (EventEmitter) (وحدة events) هو الصنف المحوري؛ وترث منه عديد من الكائنات الأصلية (خادم HTTP والتدفقات والمقابس).
  • طريقتان أساسيتان: .on(الحدث، ردّ النداء) (الاستماع) و.emit(الحدث، البيانات) (الإطلاق). وأيضًا: .once() (مرة واحدة) و.off() (الإيقاف).
  • استمع دائمًا لحدث 'error' (إصداره دون مستمع = السقوط). ففهم باعث الأحداث = فهم التعامل مع واجهات Node.

تحدي 48 ساعة — طبّق غدًا

حوّل معالجة في حلقة من كودك إلى مُصدِر: حدث لكل عنصر معالَج، وحدث نهاية بـ.once()، ومستمع error. ثم أضف مستمعًا ثانيًا (سجل، عدّاد) دون المساس بالمُصدِر.

قائمة الخروج من القسم
  • أستطيع شرح الإصدار/الاستماع وفكّ الارتباط بالاسم.
  • أعرف استخدام .on/.emit/.once/.off والاستماع لـerror.
  • فككت معالجة حقيقية بمستمعين مستقلين.
تقييم ذاتي 1-5: هل أفكّ الارتباط بالأحداث عند الحاجة؟هل أحمي كل مُصدِر بمستمع error؟

أسئلة شائعة

لماذا تحضر البرمجة الحدثية بهذه الكثافة في Node، وأين نصادفها؟

لأنها تطابق بنية Node غير المتزامنة وغير الحاجبة: طلبات تصل، وبيانات شبكة تتقدّم، وقراءات تنتهي — وكل ذلك يقع في أوقات لا تُتوقَّع، والتفاعل الطبيعي هو «عندما يصل X، افعل هذا». ونصادفها في كل مكان: يُصدِر خادم HTTP عند كل طلب، وتُصدِر التدفقات 'data'/'end'/'error' (القسم 2-3)، وتشير المقابس إلى الاتصال والإغلاق، ويُصدِر process حدث 'exit'، وتعيش مقابس الويب (القسم 4-3) على الأحداث. وكساليم، نمضي وقتنا نستمع — ومن هنا أهمية النموذج.

متى أنشئ باعث الأحداث (EventEmitter) الخاص بي بدل الردود الندائية أو الوعود؟

عندما يمكن للحدث أن يقع مرات عديدة ويكون فكّ الارتباط مهمًا. فالوعد يُحسَم مرة واحدة: مثالي لـ«نفّذ هذه العملية وأعطني نتيجتها» (قراءة ملف، استعلام قاعدة). بينما يُصدِر باعث الأحداث بقدر الحاجة لصفر أو واحد أو عدة مستمعين مستقلين: كل طلب لخادم، كل قطعة من تدفق، كل ملف من استيراد ساليم. القاعدة: نتيجة وحيدة في علاقة مباشرة = وعد/async-await؛ وتدفق متكرر ومستهلكون مفكوكون = باعث أحداث.

تحقّق من فهمك

يريد ساليم التفاعل مع كل ملف مستورَد، بقدر الحاجة، مع عرضين مستقلين. أي أداة؟

موارد إضافية (اختياري، للتعمّق أكثر)

اختبار المستوى 2أسئلة جديدة وأصعب للتحقق من مكتسباتك