Node.js: نظام الأحداث

آخر تحديث: 2026-08-26

أليس هي مهندسة برمجيات «الخلفية» تعمل على تطوير نظام إدارة الطلبات للتجارة الإلكترونية. في البداية، كانت وظيفة إنشاء الطلبات تستدعي مباشرةً ثلاث وحدات نمطية — إرسال رسائل البريد الإلكتروني، وتحديث المخزون، والتسجيل — لكن هذا كان يعني أنه في كل مرة تضيف فيها ميزة جديدة، كان عليها تعديل كود الطلبات الأساسي، مما جعل النظام أكثر هشاشة. لاحقًا، أعادت هيكلة الكود باستخدام EventEmitter: أصبحت وحدة الطلبات الآن مسؤولة فقط عن تشغيل حدث order:created، بينما تستمع كل خدمة بشكل مستقل دون أن تتداخل مع بعضها البعض. لا تتطلب إضافة «إشعارات الرسائل القصيرة» سوى إضافة مستمع، دون أي تغييرات على الكود الأساسي. وقد خفضت بنية النشر والاشتراك هذه تكاليف صيانة النظام بنسبة 60%.

1. ما ستتعلمه



2. المفاهيم الأساسية لـ EventEmitter

EventEmitter هي الفئة الأساسية للبنية القائمة على الأحداث في Node.js، وتوجد في الوحدة النمطية events. وهي تحتفظ بتعيين يربط بين أسماء الأحداث ومصفوفات من دوال المستمعين. وعندما يُطلق emit() حدثًا ما، يتم تنفيذ جميع المستمعين المسجلين بشكل متزامن بالترتيب الذي تم تسجيلهم به.

100%
flowchart LR
    subgraph Emitter["EventEmitter(Posted by)"]
        E1[emit - order:created]
    end
    subgraph Listeners["Listener(Subscribers)"]
        L1[Listener1:Send an Email]
        L2[Listener2:Update Inventory]
        L3[Listener3:Log Entry]
    end
    E1 --> L1
    E1 --> L2
    E1 --> L3
    style Emitter fill:#e1f5fe
    style Listeners fill:#f3e5f5
JAVASCRIPT
const EventEmitter = require('events');

const emitter = new EventEmitter();

emitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

emitter.emit('greet', 'Alice');
TEXT 📖 للعرض فقط
Hello, Alice!


3. مرجع سريع لأساليب EventEmitter الشائعة

الطريقة الوصف القيمة المرجعة
on(event, listener) تسجيل مستمع؛ يمكن تسجيله عدة مرات مثيل EventEmitter
once(event, listener) تسجيل مستمع لمرة واحدة يتم إزالته تلقائيًا عند حدوث الحدث مثيل EventEmitter
emit(event, ...args) تشغيل حدث، وتمرير المعلمات true يحتوي على مستمع / false لا يحتوي على مستمع
off(event, listener) إزالة مستمع محدد مثيل EventEmitter
removeListener(event, listener) مثل off()، واجهة برمجة التطبيقات القديمة مثيل EventEmitter
removeAllListeners([event]) إزالة المستمعين لجميع الأحداث أو أحداث محددة مثيل EventEmitter
prependListener(event, listener) إضافة مستمع إلى مقدمة قائمة الانتظار مثيل EventEmitter
prependOnceListener(event, listener) إضافة مستمع لمرة واحدة في الأعلى مثيل EventEmitter
listeners(event) إرجاع مصفوفة مستمعي الأحداث Function[]
listenerCount(event) عدد المستمعين number
setMaxListeners(n) تعيين الحد الأقصى لعدد المستمعين لحدث واحد مثيل EventEmitter
getMaxListeners() الحصول على الحد الأقصى لعدد المستمعين number
eventNames() إرجاع جميع أسماء الأحداث المسجلة `(string
rawListeners(event) إرجاع المستمع الأصلي المُعلَّم بـ once Function[]

▶ مثال: الاستخدام الأساسي لأوامر on/emit/off

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

function onOrderCreated(order) {
  console.log(`[Listener] Order created: #${order.id}`);
}

emitter.on('order:created', onOrderCreated);

emitter.emit('order:created', { id: 1001, product: 'Laptop', qty: 2 });

emitter.off('order:created', onOrderCreated);

const hasListeners = emitter.emit('order:created', { id: 1002, product: 'Mouse', qty: 5 });
console.log('Has listeners:', hasListeners);
▶ جرّب الكود
TEXT 📖 للعرض فقط
[Listener] Order created: #1001
Has listeners: false


4. مقارنة بين on و once و prependListener

الخصائص on() once() prependListener()
عدد المشغلات يتم تشغيلها في كل مرة يتم تشغيلها مرة واحدة فقط، ثم تُزال تلقائيًا يتم تشغيلها في كل مرة
مكان التسجيل نهاية الطابور نهاية الطابور بداية الطابور (يتم تنفيذه أولاً)
السيناريوهات النموذجية المراقبة المستمرة للرسائل يتم إجراء التهيئة مرة واحدة فقط المعالجة ذات الأولوية العالية (مثل التسجيل)
الإزالة التلقائية لا نعم لا
المطابقة للنسخة المخصصة للاستخدام مرة واحدة once() prependOnceListener()

▶ مثال: يتم تشغيل "once" مرة واحدة فقط

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.once('server:ready', (port) => {
  console.log(`Server initialized on port ${port}`);
});

emitter.emit('server:ready', 3000);
emitter.emit('server:ready', 3001);
console.log('Second emit had no effect');
▶ جرّب الكود
TEXT 📖 للعرض فقط
Server initialized on port 3000
Second emit had no effect

▶ مثال: يتم تنفيذ prependListener أولاً

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('data', () => console.log('Normal listener'));
emitter.prependListener('data', () => console.log('Priority listener'));

emitter.emit('data');
▶ جرّب الكود
TEXT 📖 للعرض فقط
Priority listener
Normal listener


5. إدارة مستمعي الأحداث

بشكل افتراضي، تسمح Node.js بحد أقصى يبلغ 10 مستمعين لكل حدث؛ وسيؤدي تجاوز هذا الحد إلى ظهور تحذير بشأن تسرب الذاكرة. وهذا ليس حدًا صارمًا، بل هو تذكير للمطورين للتحقق مما إذا كانوا قد نسوا إزالة أي مستمعين.

▶ مثال: متغير listenerCount وتحذيرات تسرب الذاكرة

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

for (let i = 0; i < 12; i++) {
  emitter.on('log', () => console.log(`Listener ${i}`));
}

console.log('Listener count:', emitter.listenerCount('log'));
console.log('Max listeners:', emitter.getMaxListeners());
▶ جرّب الكود
TEXT 📖 للعرض فقط
(node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 12 log listeners added. Use emitter.setMaxListeners() to increase limit
Listener count: 12
Max listeners: 10

▶ مثال: ضبط الحد الأعلى باستخدام setMaxListeners

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.setMaxListeners(20);

for (let i = 0; i < 15; i++) {
  emitter.on('task', () => {});
}

console.log('No warning: max set to', emitter.getMaxListeners());
console.log('Listener count:', emitter.listenerCount('task'));
▶ جرّب الكود
TEXT 📖 للعرض فقط
No warning: max set to 20
Listener count: 15

▶ مثال: عملية التنظيف removeAllListeners

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('tick', () => console.log('tick A'));
emitter.on('tick', () => console.log('tick B'));
emitter.on('tock', () => console.log('tock A'));

emitter.removeAllListeners('tick');

console.log('tick listeners:', emitter.listenerCount('tick'));
console.log('tock listeners:', emitter.listenerCount('tock'));
▶ جرّب الكود
TEXT 📖 للعرض فقط
tick listeners: 0
tock listeners: 1


6. فئات الأحداث المخصصة

في عملية التطوير الفعلية، من الشائع التوريث من EventEmitter لإنشاء فئات أحداث ذات دلالات تجارية، بدلاً من استخدام مثيلات EventEmitter مباشرةً.

▶ مثال: إنشاء فئة حدث «order» عن طريق التوسع من فئة EventEmitter

JAVASCRIPT
const EventEmitter = require('events');

class OrderEmitter extends EventEmitter {
  create(order) {
    this.emit('order:created', order);
  }

  cancel(orderId) {
    this.emit('order:cancelled', { orderId, cancelledAt: new Date().toISOString() });
  }

  ship(orderId, trackingNo) {
    this.emit('order:shipped', { orderId, trackingNo });
  }
}

const orderEvents = new OrderEmitter();

orderEvents.on('order:created', (order) => {
  console.log(`[Email] Confirmation for order #${order.id}`);
});

orderEvents.on('order:created', (order) => {
  console.log(`[Inventory] Deduct ${order.qty}x ${order.product}`);
});

orderEvents.on('order:cancelled', ({ orderId }) => {
  console.log(`[Refund] Processing refund for #${orderId}`);
});

orderEvents.create({ id: 2001, product: 'Headphones', qty: 3 });
orderEvents.cancel(2001);
▶ جرّب الكود
TEXT 📖 للعرض فقط
[Email] Confirmation for order #2001
[Inventory] Deduct 3x Headphones
[Refund] Processing refund for #2001


7. معالجة الأحداث والأخطاء في error

يحتوي EventEmitter على حدث خاص يُسمى error: إذا تم تشغيل الحدث error ولكن لم تكن هناك أي مستمعات، فسوف يقوم Node.js بإصدار استثناء وإنهاء العملية. وهذه هي قاعدة الأمان الأهم في EventEmitter.

▶ مثال: تعطل العملية بسبب عدم معالجة خطأ ما

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.emit('error', new Error('Database connection failed'));
▶ جرّب الكود
TEXT 📖 للعرض فقط
events.js:291
      throw er; // Unhandled 'error' event
      ^
Error: Database connection failed
    at Object.<anonymous> (app.js:4:20)

▶ مثال: التعامل مع الحدث error بشكل صحيح

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('error', (err) => {
  console.error(`[Error Handler] ${err.message}`);
});

emitter.emit('error', new Error('Database connection failed'));
console.log('Process continues running');
▶ جرّب الكود
TEXT 📖 للعرض فقط
[Error Handler] Database connection failed
Process continues running


8. مقارنة بين النمط القائم على الأحداث، ووظائف الاستدعاء العكسي، والوعود

الميزة مدفوعة بالأحداث (EventEmitter) الاستدعاء المرتد Promise / async-await
نمط الاتصال واحد إلى عدة (النشر والاشتراك) واحد إلى واحد واحد إلى واحد
عدد المشغلات يمكن تشغيلها عدة مرات يتم تشغيلها مرة واحدة فقط يتم حلها مرة واحدة فقط
السيناريوهات النموذجية بث الرسائل، تغييرات الحالة إتمام عمليات الإدخال/الإخراج النتائج النهائية للعمليات غير المتزامنة
التوصيل منخفض (الناشر لا يعرف المشترك) مرتفع (يجب أن يعرف المتصل عنوان الرد) متوسط (التسلسل أو await)
قابل للإلغاء يمكن إزالته باستخدام off() غير قابل للإلغاء غير قابل للإلغاء (يمكن تجاهله)
الدعم المدمج وحدة events القواعد العالمية اللغة المدمجة
معالجة الأخطاء error الحدث استدعاء رد الفعل لأولوية الخطأ .catch() / try-catch


9. الوحدات المدمجة في Node.js التي تستخدم EventEmitter

ترث العديد من الوحدات المدمجة في Node.js خصائص EventEmitter؛ فجميع كائنات التدفق (stream) والخادم (server) والعملية (process) تقريبًا هي بواعث أحداث.

الوحدة/الكائن يرث من EventEmitter الأحداث الشائعة
net.Server نعم connection، close، error
http.Server نعم request، connection، close
stream.Readable نعم data، end، error، close
stream.Writable نعم drain، finish، error، close
net.Socket نعم data، connect، end، error
process نعم exit، uncaughtException، SIGINT
child_process.ChildProcess نعم exit، message، error، close
fs.watch() العودة إلى EventEmitter change، error

▶ مثال: http.Server باستخدام EventEmitter

JAVASCRIPT
const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
  console.log(`[Request] ${req.method} ${req.url}`);
  res.end('OK');
});

server.on('connection', (socket) => {
  console.log(`[Connection] New client from ${socket.remoteAddress}`);
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});
▶ جرّب الكود
TEXT 📖 للعرض فقط
Server listening on port 3000
[Connection] New client from ::ffff:127.0.0.1
[Request] GET /

▶ مثال: دفق قابل للقراءة باستخدام EventEmitter

JAVASCRIPT
const { Readable } = require('stream');

const readable = Readable.from(['Hello', ' ', 'World']);

readable.on('data', (chunk) => {
  console.log(`[Data] Received: "${chunk}"`);
});

readable.on('end', () => {
  console.log('[End] No more data');
});
▶ جرّب الكود
TEXT 📖 للعرض فقط
[Data] Received: "Hello"
[Data] Received: " "
[Data] Received: "World"
[End] No more data


10. مثال شامل: برنامج جدولة المهام المدفوع بالأحداث

أنشئ فئة TaskScheduler تدعم تسجيل المهام، وتشغيل الأحداث، والاستجابات من مستمعين متعددين. قم بمحاكاة سلسلة الإشعارات التي تحدث بعد إنشاء مهمة في سيناريو واقعي.

JAVASCRIPT
const EventEmitter = require('events');

class TaskScheduler extends EventEmitter {
  constructor() {
    super();
    this.tasks = new Map();
    this.nextId = 1;
  }

  addTask(name, payload) {
    const id = this.nextId++;
    const task = {
      id,
      name,
      payload,
      createdAt: new Date().toISOString(),
      status: 'pending'
    };
    this.tasks.set(id, task);
    this.emit('task:added', task);
    return task;
  }

  startTask(id) {
    const task = this.tasks.get(id);
    if (!task) {
      this.emit('error', new Error(`Task #${id} not found`));
      return;
    }
    task.status = 'running';
    task.startedAt = new Date().toISOString();
    this.emit('task:started', task);
  }

  completeTask(id, result) {
    const task = this.tasks.get(id);
    if (!task) {
      this.emit('error', new Error(`Task #${id} not found`));
      return;
    }
    task.status = 'completed';
    task.result = result;
    task.completedAt = new Date().toISOString();
    this.emit('task:completed', task);
  }

  failTask(id, reason) {
    const task = this.tasks.get(id);
    if (!task) {
      this.emit('error', new Error(`Task #${id} not found`));
      return;
    }
    task.status = 'failed';
    task.reason = reason;
    task.failedAt = new Date().toISOString();
    this.emit('task:failed', task);
  }

  getStats() {
    const stats = { total: this.tasks.size, pending: 0, running: 0, completed: 0, failed: 0 };
    for (const task of this.tasks.values()) {
      stats[task.status]++;
    }
    return stats;
  }
}

const scheduler = new TaskScheduler();

scheduler.on('error', (err) => {
  console.error(`[ERROR] ${err.message}`);
});

scheduler.on('task:added', (task) => {
  console.log(`[Logger] Task #${task.id} "${task.name}" added at ${task.createdAt}`);
});

scheduler.on('task:added', (task) => {
  console.log(`[Notifier] New task available: ${task.name}`);
});

scheduler.on('task:started', (task) => {
  console.log(`[Executor] Task #${task.id} is now running...`);
});

scheduler.on('task:completed', (task) => {
  console.log(`[Reporter] Task #${task.id} completed with result: ${task.result}`);
  console.log(`[Cleaner] Releasing resources for task #${task.id}`);
});

scheduler.on('task:failed', (task) => {
  console.log(`[Alerter] Task #${task.id} failed: ${task.reason}`);
});

console.log('=== Adding Tasks ===');
const t1 = scheduler.addTask('Data Import', { source: 'api.example.com', rows: 5000 });
const t2 = scheduler.addTask('Report Generation', { format: 'PDF', quarter: 'Q4' });

console.log('\n=== Starting Tasks ===');
scheduler.startTask(t1.id);
scheduler.startTask(t2.id);

console.log('\n=== Completing / Failing Tasks ===');
scheduler.completeTask(t1.id, '5000 rows imported successfully');
scheduler.failTask(t2.id, 'PDF renderer service unavailable');

console.log('\n=== Stats ===');
console.log(scheduler.getStats());

console.log('\n=== Invalid Operation ===');
scheduler.startTask(999);
TEXT 📖 للعرض فقط
=== Adding Tasks ===
[Logger] Task #1 "Data Import" added at 2025-07-03T10:00:00.000Z
[Notifier] New task available: Data Import
[Logger] Task #2 "Report Generation" added at 2025-07-03T10:00:01.000Z
[Notifier] New task available: Report Generation

=== Starting Tasks ===
[Executor] Task #1 is now running...
[Executor] Task #2 is now running...

=== Completing / Failing Tasks ===
[Reporter] Task #1 completed with result: 5000 rows imported successfully
[Cleaner] Releasing resources for task #1
[Alerter] Task #2 failed: PDF renderer service unavailable

=== Stats ===
{ total: 2, pending: 0, running: 0, completed: 1, failed: 1 }

=== Invalid Operation ===
[ERROR] Task #999 not found


❓ أسئلة شائعة

س ما الفرق بين EventEmitter وأحداث المتصفح؟
ج يعمل EventEmitter في Node.js بشكل متزامن، حيث يتم تشغيل المستمعين بالترتيب الذي تم تسجيلهم به؛ أما أحداث DOM في المتصفح فهي غير متزامنة، وتمر بمراحل «الالتقاط» و«الانتشار»، وتتميز ببنية كائن أحداث مختلفة عن تلك الموجودة في Node.js.
س ماذا يحدث إذا نسيت الاستماع إلى الحدث error؟
ج إذا لم يكن هناك أي مستمعين مسجلين لـ error عند استدعاء emit('error')، فسوف يُصدر Node.js هذا الخطأ كاستثناء لم يتم التقاطه، وستنتهي العملية على الفور (ما لم يتم تعيين process.on('uncaughtException')).
س ما هو الحد الأقصى لعدد المستمعين الذين يمكن أن يستوعبهم الحدث؟
ج الحد الافتراضي هو 10. وسيؤدي تجاوز هذا الحد إلى ظهور تحذير بشأن تسرب الذاكرة (ولكنه لن يعيق التنفيذ). ويمكن تعديل هذا الحد عبر emitter.setMaxListeners(n)؛ حيث يؤدي تعيينه على 0 إلى إزالة الحد.
س ما هي قيمة الإرجاع لـ emit()؟
ج إذا كان للحدث مستمع واحد على الأقل، فإنه يُرجع true؛ أما إذا لم يكن له أي مستمعين، فإنه يُرجع false. ويمكن استخدام ذلك لتحديد ما إذا كان الحدث قد تمت معالجته أم لا.
س كيف يمكنني التأكد من إزالة المستمع تلقائيًا بعد تشغيله مرة واحدة فقط؟
ج استخدم الطريقة once() لتسجيل المستمع؛ حيث سيتم إزالته تلقائيًا بعد تشغيله مرة واحدة. وهذا مناسب لحالات مثل التهيئة والإشارات التي تُستخدم لمرة واحدة.
س ما الفرق بين off() وremoveListener()؟
ج تعملان بنفس الطريقة تمامًا. off() هو اسم مستعار جديد أُضيف في الإصدار 10 من Node.js removeListener() ويوفر صيغة أكثر إيجازًا؛ ويُنصح باستخدام off() في الكود الجديد.
س ما الذي يشير إليه this في المستمع؟
ج عند استخدام دوال السهم في ES6، يرث this النطاق الخارجي؛ وعند استخدام الدوال العادية، يشير this إلى مثيل EventEmitter، ما لم يتم ربطه بكائن آخر عبر bind().

📖 ملخص


📝 تمارين

  1. أكمل جميع أمثلة الأكواد الواردة في هذا الدرس وتأكد من أن كل منها يعمل بشكل صحيح.
  2. قم بتعديل المثال الشامل وأضف الإضافات الخاصة بك
  3. راجع الوثائق الرسمية، وحدد واجهة برمجة تطبيقات (API) واحدة أو اثنتين لم يتم تناولهما في هذا الدرس، واكتب كود اختبار لهما.
  4. التأمل: كيف ستطبق ما تعلمته في هذا الدرس على مشروع في الواقع العملي؟
  5. حاول أن تجمع بين ما تعلمته في هذا الدرس والمواد التي درستها في الدروس السابقة لإنشاء مشروع صغير.
Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%