Node.js: البرمجة غير المتزامنة
آخر تحديث: 2026-08-26
1. القصة: خدمة تم حظرها لمدة 200 مللي ثانية
عادةً ما تستجيب خدمة واجهة برمجة التطبيقات (API) الخاصة بـ«بوب» في غضون 5 مللي ثانية فقط، ولكن خلال ساعات الذروة، ارتفعت أوقات الاستجابة بشكل حاد لتصل إلى 2,000 مللي ثانية. استمر المستخدمون في تقديم الشكاوى، وفي البداية، اعتقد «بوب» أن قاعدة البيانات كانت بطيئة، لكن إضافة الفهارس لم تساعد. لاحقًا، باستخدام console.time لتشخيص المشكلة خطوة بخطوة، اكتشف أن فرز مصفوفة JSON متزامنة كبيرة كان يستغرق 200 مللي ثانية — وكانت هذه العملية التي تستغرق 200 مللي ثانية تعطل حلقة الأحداث، مما تسبب في انتظار جميع الطلبات الموجودة في قائمة الانتظار حتى تنتهي. بعد فهم كيفية عمل حلقة الأحداث، استخدم بوب setImmediate لتقسيم عملية الفرز إلى أجزاء أصغر وتنفيذها بالتتابع، مما أدى إلى تقليل أوقات الاستجابة خلال ساعات الذروة إلى 50 مللي ثانية.
ستتعلم:
- المسؤوليات وتسلسل المراحل الست لحلقة الأحداث
- أولوية قائمة انتظار المهام الصغيرة (process.nextTick / Promise.then)
- جدولة قوائم انتظار المهام الكبرى (setTimeout / setImmediate / I/O)
- ترتيب التنفيذ بين setTimeout و setImmediate و .nextTick في العملية
- لماذا يحظى
setImmediateبالأولوية علىsetTimeoutفي سياق الإدخال/الإخراج - مخاطر حجب حلقة الأحداث واستراتيجيات تقسيمها
2. المراحل الست لحلقة الأحداث
تعمل حلقة الأحداث في Node.js بواسطة مكتبة libuv وتمر بست مراحل متتالية. تبدأ كل دورة من الدورة بـ«المؤقتات» وتنتهي بـ«استدعاءات الإغلاق»، ثم تعود إلى «المؤقتات» لبدء الدورة التالية.
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
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');
});
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
▶ مثال: ترتيب تنفيذ السياقات الخارجية غير محدد
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
في سياق غير متعلق بعمليات الإدخال/الإخراج، يكون ترتيب تنفيذ setTimeout وsetImmediate غير محدد — فهو يعتمد على جدولة العمليات في النظام، ويمكن أن يتم تنفيذ أي منهما أولاً.
▶ مثال: في سياق الإدخال/الإخراج، يتم دائمًا تنفيذ setImmediate أولاً
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
setImmediate
setTimeout
يتم تنفيذ عمليات الاستدعاء المرتدة لعمليات الإدخال/الإخراج (I/O) خلال مرحلة الاستقصاء؛ وبمجرد اكتمالها، تنتقل العملية إلى مرحلة الفحص (setImmediate)، ولا تنتقل إلى الجولة التالية من مرحلة المؤقتات (setTimeout) إلا بعد ذلك.
▶ مثال: process.nextTick يأتي دائمًا في المرتبة الأولى
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
process.nextTick(() => {
console.log('nextTick');
});
nextTick
setTimeout (or setImmediate, depends on scheduling)
setImmediate
| الميزة | setTimeout(fn, 0) | setImmediate | process.nextTick |
|---|---|---|---|
| قائمة الانتظار | المهام الكبيرة (المؤقتات) | المهام الكبيرة (التحقق) | المهام الصغيرة (الأولوية القصوى) |
| مرحلة التنفيذ | المؤقتات | الفحص | فور انتهاء المرحلة الحالية |
| الحد الأدنى لزمن الاستجابة | 1 مللي ثانية | لا شيء | لا شيء |
| ترتيب سياق الإدخال/الإخراج | بعد setImmediate | قبل setTimeout | الأول |
| ترتيب السياق الخارجي | غير محدد | غير محدد | الأقدم |
5. مقارنة بين المهام الصغيرة والمهام الكبيرة
| البعد | المهمة الصغيرة | المهمة الكبيرة |
|---|---|---|
| الممثل | process.nextTick، Promise.then | setTimeout، setImmediate، استدعاءات الإرجاع الخاصة بعمليات الإدخال/الإخراج |
| توقيت التنفيذ | بعد انتهاء المرحلة الحالية وقبل بدء المرحلة التالية | أثناء المرحلة المقابلة من حلقة الأحداث |
| استراتيجية التنفيذ | مسح الكل دفعة واحدة | تنفيذ مهمة واحدة في كل مرحلة، ثم التحقق من المهام الصغيرة |
| متداخلة | يتم أيضًا مسح المهام الصغيرة التي تم إنشاؤها أثناء التنفيذ في هذه الجولة | يتم تأجيلها إلى مرحلة لاحقة للتنفيذ |
| الأولوية | أعلى من جميع المهام الكلية | أقل من المهام الجزئية |
▶ مثال: المهام الصغيرة المتخللة بين المهام الكبيرة
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => {
console.log('promise between 1 and 2');
});
}, 0);
setTimeout(() => {
console.log('timeout 2');
}, 0);
timeout 1
promise between 1 and 2
timeout 2
بعد الانتهاء من كل مهمة كبرى، يتم مسح قائمة انتظار المهام الصغيرة قبل تنفيذ المهمة الكبرى التالية.
6. مخاطر تعطيل حلقة الأحداث وكيفية تجنبها
▶ مثال: إعاقة حلقة الأحداث
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
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. مثال شامل: برنامج نصي للتحقق من ترتيب تنفيذ حلقة الأحداث
يتحقق البرنامج النصي التالي من تسلسل التنفيذ الكامل لـ setTimeout → process.nextTick → setImmediate → Promise.then → استدعاء I/O.
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 ---');
--- 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
تحليل ترتيب التنفيذ:
- يتم تشغيل الكود المتزامن أولاً، ويُخرج «Script start» و«Script end»
- إنجاز المهام الصغيرة بعد انتهاء المرحلة الحالية: nextTick → Promise.then
- الدخول إلى مرحلة «المؤقتات»: setTimeout
- الدخول إلى مرحلة الفحص: setImmediate
- تنفيذ عملية الاستدعاء المرتد خلال مرحلة الاستقصاء بعد اكتمال عملية الإدخال/الإخراج
- مسح المهمة الصغيرة بعد اكتمال استدعاء رد الإدخال/الإخراج: inner nextTick → inner Promise.then
- عند انتهاء الاستطلاع، أدخل الأمر التالي: inner setImmediate
- الجولة التالية من المؤقتات: setTimeout الداخلي
❓ أسئلة شائعة
setTimeout(fn, 0) فعلاً بعد 0 مللي ثانية؟process.nextTick أم Promise.then؟process.nextTick أولاً. تتمتع nextTick بأعلى أولوية في قائمة انتظار المهام الصغيرة، ويتم مسح قائمة انتظار nextTick دائمًا قبل قائمة انتظار Promise.setImmediate قبل setTimeout في استدعاء رد الإدخال/الإخراج؟poll. وبمجرد انتهاء تلك المرحلة، تنتقل العملية مباشرةً إلى مرحلة check (setImmediate) قبل الانتقال إلى الجولة التالية من مرحلة timers (setTimeout). ولذلك، في سياق الإدخال/الإخراج، يتم دائمًا تنفيذ setImmediate قبل setTimeout.setImmediate؛ واستخدم خيوط العمل (worker threads) لنقل المهام التي تستهلك موارد كبيرة من وحدة المعالجة المركزية (CPU) إلى خيوط منفصلة؛ واستخدم الإدخال/الإخراج غير المتزامن (asynchronous I/O) بدلاً من الإدخال/الإخراج المتزامن (synchronous I/O)؛ واستخدم التدفقات (streams) لمعالجة مجموعات البيانات الكبيرة على شكل أجزاء.process.nextTick بشكل متكرر إلى ما لا نهاية؟process.maxTickDepth؛ وستؤدي الاستدعاءات المتكررة لـ nextTick إلى منع حلقة الأحداث من الانتقال إلى المرحلة التالية، مما يؤدي إلى نقص في عمليات الإدخال/الإخراج. يجب عليك استخدام setImmediate بدلاً من الاستدعاءات التكرارية لـ nextTick.📖 ملخص
- المقال: المفاهيم الأساسية واستخدام «خدمة محجوبة لمدة 200 مللي ثانية»
- المفاهيم الأساسية واستخدامات المراحل الست لحلقة الأحداث
- المفاهيم الأساسية للمهام الصغيرة والمهام الكبيرة وكيفية استخدامها
- المفاهيم الأساسية واستخدامات setTimeout و setImmediate و process.nextTick
- المفاهيم الأساسية واستخدامات المهام الصغيرة مقابل المهام الكبيرة
- مخاطر حجب حلقة الأحداث، والمفاهيم الأساسية لتجنبها، وكيفية القيام بذلك
- مثال شامل: التحقق من ترتيب تنفيذ حلقة الأحداث — المفاهيم الأساسية واستخدام البرامج النصية
📝 تمارين
- أكمل جميع أمثلة الأكواد الواردة في هذا الدرس وتأكد من أن كل منها يعمل بشكل صحيح.
- قم بتعديل المثال الشامل وأضف الإضافات الخاصة بك
- راجع الوثائق الرسمية، وحدد واجهة برمجة تطبيقات (API) واحدة أو اثنتين لم يتم تناولهما في هذا الدرس، واكتب كود اختبار لهما.
- التأمل: كيف ستطبق ما تعلمته في هذا الدرس على مشروع في الواقع العملي؟
- حاول أن تجمع بين ما تعلمته في هذا الدرس والمواد التي درستها في الدروس السابقة لإنشاء مشروع صغير.