Node.js: البرمجة غير المتزامنة

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

1. القصة: خدمة تم حظرها لمدة 200 مللي ثانية

عادةً ما تستجيب خدمة واجهة برمجة التطبيقات (API) الخاصة بـ«بوب» في غضون 5 مللي ثانية فقط، ولكن خلال ساعات الذروة، ارتفعت أوقات الاستجابة بشكل حاد لتصل إلى 2,000 مللي ثانية. استمر المستخدمون في تقديم الشكاوى، وفي البداية، اعتقد «بوب» أن قاعدة البيانات كانت بطيئة، لكن إضافة الفهارس لم تساعد. لاحقًا، باستخدام console.time لتشخيص المشكلة خطوة بخطوة، اكتشف أن فرز مصفوفة JSON متزامنة كبيرة كان يستغرق 200 مللي ثانية — وكانت هذه العملية التي تستغرق 200 مللي ثانية تعطل حلقة الأحداث، مما تسبب في انتظار جميع الطلبات الموجودة في قائمة الانتظار حتى تنتهي. بعد فهم كيفية عمل حلقة الأحداث، استخدم بوب setImmediate لتقسيم عملية الفرز إلى أجزاء أصغر وتنفيذها بالتتابع، مما أدى إلى تقليل أوقات الاستجابة خلال ساعات الذروة إلى 50 مللي ثانية.

ستتعلم:



2. المراحل الست لحلقة الأحداث

تعمل حلقة الأحداث في Node.js بواسطة مكتبة libuv وتمر بست مراحل متتالية. تبدأ كل دورة من الدورة بـ«المؤقتات» وتنتهي بـ«استدعاءات الإغلاق»، ثم تعود إلى «المؤقتات» لبدء الدورة التالية.

100%
flowchart TD
    A["timers<br/>setTimeout / setInterval"] --> B["pending callbacks<br/>System-Level Callbacks"]
    B --> C["idle, prepare<br/>For internal use only"]
    C --> D["poll<br/>I/O callbacks / new I/O polling"]
    D --> E["check<br/>setImmediate"]
    E --> F["close callbacks<br/>socket.on('close')"]
    F --> A
المرحلة نوع الاستدعاء الوصف
المؤقتات setTimeout / setInterval تنفذ دالة الاستدعاء المرتبطة بالمؤقت عند انتهاء مدته، مع مراعاة تأخير أدنى يبلغ 1 مللي ثانية
عمليات الاستدعاء المعلقة عمليات الاستدعاء على مستوى النظام عمليات الاستدعاء الخاصة بعمليات الإدخال/الإخراج المؤجلة إلى الدورة التالية (مثل أخطاء TCP)
في وضع الخمول، التحضير للاستخدام الداخلي للاستخدام الداخلي بواسطة libuv؛ ولا يتعامل المطورون معها عادةً
استطلاع استدعاء الإرجاع الخاص بعمليات الإدخال/الإخراج يتحقق من وجود أحداث إدخال/إخراج جديدة ويقوم بتنفيذ استدعاءات الإرجاع المتعلقة بعمليات الإدخال/الإخراج؛ وفي حالة عدم وجود مؤقتات، ستتوقف العملية في هذه المرحلة
check setImmediate يتم تنفيذ دالة الاستدعاء setImmediate في هذه المرحلة
دالات الإغلاق أحداث الإغلاق مثل socket.on('close') دالات الإغلاق


3. المهام الصغيرة والمهام الكبيرة

(1) (3.1) قائمة انتظار المهام الصغيرة

يتم تنفيذ المهام الصغيرة بالكامل بعد انتهاء المرحلة الحالية وقبل بدء المرحلة التالية. وتحتوي قائمة انتظار المهام الصغيرة على مستويين من الأولوية:

المهمة الصغيرة الأولوية الوصف
process.nextTick الحد الأقصى إعطاء الأولوية لمسح قائمة انتظار nextTick بعد انتهاء كل مرحلة
Promise.then / catch / finally ثاني أعلى يتم تنفيذه بعد مسح قائمة انتظار nextTick

▶ مثال: nextTick له الأسبقية على Promise.then

JAVASCRIPT
process.nextTick(() => {
  console.log('nextTick 1');
});

Promise.resolve().then(() => {
  console.log('promise.then 1');
});

process.nextTick(() => {
  console.log('nextTick 2');
});

Promise.resolve().then(() => {
  console.log('promise.then 2');
});
▶ جرّب الكود
TEXT 📖 للعرض فقط
nextTick 1
nextTick 2
promise.then 1
promise.then 2

(2) (3.2) قائمة انتظار المهام الكبيرة

يتم جدولة المهام الكبيرة في كل مرحلة من مراحل حلقة الأحداث، حيث تقوم كل مرحلة بتنفيذ المهام الكبيرة المرتبطة بها.

المهمة الكلية المرحلة الوصف
setTimeout / setInterval المؤقتات الحد الأدنى للتأخير هو 1 مللي ثانية؛ تدخل قائمة انتظار المؤقتات عند انتهاء المدة
setImmediate check يتم تنفيذه خلال مرحلة "check" من الدورة التالية
استدعاء رد الإدخال/الإخراج poll استدعاء رد عند اكتمال عمليات الإدخال/الإخراج مثل قراءة الملفات وكتابتها وعمليات الشبكة
إغلاق إجراءات الاستدعاء إغلاق إجراءات الاستدعاء إغلاق إجراءات الاستدعاء الخاصة بالأحداث


4. setTimeout مقابل setImmediate مقابل process.nextTick

▶ مثال: ترتيب تنفيذ السياقات الخارجية غير محدد

JAVASCRIPT
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});
▶ جرّب الكود

في سياق غير متعلق بعمليات الإدخال/الإخراج، يكون ترتيب تنفيذ setTimeout وsetImmediate غير محدد — فهو يعتمد على جدولة العمليات في النظام، ويمكن أن يتم تنفيذ أي منهما أولاً.

▶ مثال: في سياق الإدخال/الإخراج، يتم دائمًا تنفيذ setImmediate أولاً

JAVASCRIPT
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('setImmediate');
  });
});
▶ جرّب الكود
TEXT 📖 للعرض فقط
setImmediate
setTimeout

يتم تنفيذ عمليات الاستدعاء المرتدة لعمليات الإدخال/الإخراج (I/O) خلال مرحلة الاستقصاء؛ وبمجرد اكتمالها، تنتقل العملية إلى مرحلة الفحص (setImmediate)، ولا تنتقل إلى الجولة التالية من مرحلة المؤقتات (setTimeout) إلا بعد ذلك.

▶ مثال: process.nextTick يأتي دائمًا في المرتبة الأولى

JAVASCRIPT
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

process.nextTick(() => {
  console.log('nextTick');
});
▶ جرّب الكود
TEXT 📖 للعرض فقط
nextTick
setTimeout   (or setImmediate, depends on scheduling)
setImmediate
الميزة setTimeout(fn, 0) setImmediate process.nextTick
قائمة الانتظار المهام الكبيرة (المؤقتات) المهام الكبيرة (التحقق) المهام الصغيرة (الأولوية القصوى)
مرحلة التنفيذ المؤقتات الفحص فور انتهاء المرحلة الحالية
الحد الأدنى لزمن الاستجابة 1 مللي ثانية لا شيء لا شيء
ترتيب سياق الإدخال/الإخراج بعد setImmediate قبل setTimeout الأول
ترتيب السياق الخارجي غير محدد غير محدد الأقدم


5. مقارنة بين المهام الصغيرة والمهام الكبيرة

البعد المهمة الصغيرة المهمة الكبيرة
الممثل process.nextTick، Promise.then setTimeout، setImmediate، استدعاءات الإرجاع الخاصة بعمليات الإدخال/الإخراج
توقيت التنفيذ بعد انتهاء المرحلة الحالية وقبل بدء المرحلة التالية أثناء المرحلة المقابلة من حلقة الأحداث
استراتيجية التنفيذ مسح الكل دفعة واحدة تنفيذ مهمة واحدة في كل مرحلة، ثم التحقق من المهام الصغيرة
متداخلة يتم أيضًا مسح المهام الصغيرة التي تم إنشاؤها أثناء التنفيذ في هذه الجولة يتم تأجيلها إلى مرحلة لاحقة للتنفيذ
الأولوية أعلى من جميع المهام الكلية أقل من المهام الجزئية

▶ مثال: المهام الصغيرة المتخللة بين المهام الكبيرة

JAVASCRIPT
setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => {
    console.log('promise between 1 and 2');
  });
}, 0);

setTimeout(() => {
  console.log('timeout 2');
}, 0);
▶ جرّب الكود
TEXT 📖 للعرض فقط
timeout 1
promise between 1 and 2
timeout 2

بعد الانتهاء من كل مهمة كبرى، يتم مسح قائمة انتظار المهام الصغيرة قبل تنفيذ المهمة الكبرى التالية.



6. مخاطر تعطيل حلقة الأحداث وكيفية تجنبها

▶ مثال: إعاقة حلقة الأحداث

JAVASCRIPT
const http = require('http');

const server = http.createServer((req, res) => {
  const start = Date.now();
  while (Date.now() - start < 200) {}
  res.end('done');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});
▶ جرّب الكود

يؤدي كل طلب إلى تعطيل حلقة الأحداث لمدة 200 مللي ثانية، ولا يمكن خلال هذه المدة معالجة أي طلبات أخرى.

▶ مثال: تقسيم المهام طويلة الأمد باستخدام setImmediate

JAVASCRIPT
function chunkedSort(arr, compareFn, callback) {
  const CHUNK = 1000;
  let offset = 0;

  function sortChunk() {
    const end = Math.min(offset + CHUNK, arr.length);
    for (let i = offset; i < end; i++) {
      for (let j = i + 1; j < end; j++) {
        if (compareFn(arr[i], arr[j]) > 0) {
          [arr[i], arr[j]] = [arr[j], arr[i]];
        }
      }
    }
    offset = end;
    if (offset < arr.length) {
      setImmediate(sortChunk);
    } else {
      callback(arr);
    }
  }

  setImmediate(sortChunk);
}

chunkedSort(
  [5, 3, 8, 1, 9, 2, 7, 4, 6],
  (a, b) => a - b,
  (result) => {
    console.log('Sorted:', result);
  }
);
▶ جرّب الكود
البعد الكود المعطل الكود غير المعطل
طريقة الحساب إكمال العملية في حلقة متزامنة واحدة تقسيمها إلى أجزاء صغيرة وتنفيذها بالتتابع باستخدام setImmediate
حلقة الأحداث متوقفة؛ الطلبات الأخرى معلقة يتم التنازل عن التحكم بين الشرائح؛ ويمكن تنفيذ عمليات الاستدعاء المرتدة الأخرى
وقت الاستجابة التأخير التراكمي التباين في التأخير (المقبول لدى المستخدم)
استخدام وحدة المعالجة المركزية نواة واحدة 100% (ارتفاعات مفاجئة) استخدام موزع، مع إتاحة وقت لعمليات الإدخال/الإخراج
حالات الاستخدام العمليات الحسابية القصيرة جدًّا العمليات الحسابية الطويلة، والخدمات عالية التزامن


7. مثال شامل: برنامج نصي للتحقق من ترتيب تنفيذ حلقة الأحداث

يتحقق البرنامج النصي التالي من تسلسل التنفيذ الكامل لـ setTimeoutprocess.nextTicksetImmediatePromise.then → استدعاء I/O.

JAVASCRIPT
const fs = require('fs');

console.log('--- Script start ---');

setTimeout(() => {
  console.log('1. setTimeout (macrotask - timers)');
}, 0);

setImmediate(() => {
  console.log('2. setImmediate (macrotask - check)');
});

process.nextTick(() => {
  console.log('3. process.nextTick (microtask - highest)');
});

Promise.resolve().then(() => {
  console.log('4. Promise.then (microtask)');
});

fs.readFile(__filename, () => {
  console.log('5. I/O callback (poll phase)');

  setTimeout(() => {
    console.log('6.   inner setTimeout');
  }, 0);

  setImmediate(() => {
    console.log('7.   inner setImmediate');
  });

  process.nextTick(() => {
    console.log('8.   inner nextTick');
  });

  Promise.resolve().then(() => {
    console.log('9.   inner Promise.then');
  });
});

console.log('--- Script end ---');
TEXT 📖 للعرض فقط
--- Script start ---
--- Script end ---
3. process.nextTick (microtask - highest)
4. Promise.then (microtask)
1. setTimeout (macrotask - timers)
2. setImmediate (macrotask - check)
5. I/O callback (poll phase)
8.   inner nextTick
9.   inner Promise.then
7.   inner setImmediate
6.   inner setTimeout

تحليل ترتيب التنفيذ:

  1. يتم تشغيل الكود المتزامن أولاً، ويُخرج «Script start» و«Script end»
  2. إنجاز المهام الصغيرة بعد انتهاء المرحلة الحالية: nextTick → Promise.then
  3. الدخول إلى مرحلة «المؤقتات»: setTimeout
  4. الدخول إلى مرحلة الفحص: setImmediate
  5. تنفيذ عملية الاستدعاء المرتد خلال مرحلة الاستقصاء بعد اكتمال عملية الإدخال/الإخراج
  6. مسح المهمة الصغيرة بعد اكتمال استدعاء رد الإدخال/الإخراج: inner nextTick → inner Promise.then
  7. عند انتهاء الاستطلاع، أدخل الأمر التالي: inner setImmediate
  8. الجولة التالية من المؤقتات: setTimeout الداخلي


❓ أسئلة شائعة

س هل يتم تنفيذ setTimeout(fn, 0) فعلاً بعد 0 مللي ثانية؟
ج لا. فكل من المتصفحات وNode.js لديها تأخير أدنى (حوالي 1 مللي ثانية)، ويجب أن ينتظر التنفيذ حتى تدخل حلقة الأحداث مرحلة «المؤقتات»؛ ويعتمد وقت الانتظار الفعلي على المرحلة الحالية.
س أيهما يُنفَّذ أولاً، process.nextTick أم Promise.then؟
ج يُنفَّذ process.nextTick أولاً. تتمتع nextTick بأعلى أولوية في قائمة انتظار المهام الصغيرة، ويتم مسح قائمة انتظار nextTick دائمًا قبل قائمة انتظار Promise.
س لماذا يتم تنفيذ setImmediate قبل setTimeout في استدعاء رد الإدخال/الإخراج؟
ج يتم تنفيذ استدعاءات رد الإدخال/الإخراج خلال مرحلة poll. وبمجرد انتهاء تلك المرحلة، تنتقل العملية مباشرةً إلى مرحلة check (setImmediate) قبل الانتقال إلى الجولة التالية من مرحلة timers (setTimeout). ولذلك، في سياق الإدخال/الإخراج، يتم دائمًا تنفيذ setImmediate قبل setTimeout.
س هل حلقة الأحداث أحادية الخيط أم متعددة الخيوط؟
ج يتم تنفيذ جافا سكريبت في خيط واحد، لكن مكتبة libuv الأساسية تحتوي على مجموعة خيوط (4 خيوط بشكل افتراضي) تتولى معالجة عمليات مثل عمليات البحث في نظام أسماء النطاقات (DNS) وعمليات الإدخال/الإخراج للملفات. أما عمليات الإدخال/الإخراج الخاصة بالشبكة، فتتولى معالجتها نواة نظام التشغيل، التي تُخطر حلقة الأحداث بمجرد اكتمالها.
س كيف يمكن تجنب إعاقة حلقة الأحداث؟
ج قم بتقسيم العمليات الحسابية الطويلة إلى أجزاء أصغر وتنفيذها خطوة بخطوة باستخدام setImmediate؛ واستخدم خيوط العمل (worker threads) لنقل المهام التي تستهلك موارد كبيرة من وحدة المعالجة المركزية (CPU) إلى خيوط منفصلة؛ واستخدم الإدخال/الإخراج غير المتزامن (asynchronous I/O) بدلاً من الإدخال/الإخراج المتزامن (synchronous I/O)؛ واستخدم التدفقات (streams) لمعالجة مجموعات البيانات الكبيرة على شكل أجزاء.
س هل يمكن استدعاء process.nextTick بشكل متكرر إلى ما لا نهاية؟
ج لا يُنصح بذلك. يوجد في Node.js حد افتراضي محدد بواسطة process.maxTickDepth؛ وستؤدي الاستدعاءات المتكررة لـ nextTick إلى منع حلقة الأحداث من الانتقال إلى المرحلة التالية، مما يؤدي إلى نقص في عمليات الإدخال/الإخراج. يجب عليك استخدام setImmediate بدلاً من الاستدعاءات التكرارية لـ nextTick.

📖 ملخص


📝 تمارين

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

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

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

100%