Node.js / الأساسي: بيئة تشغيل Node.js / الدرس 3
الوعود وحلقة الأحداث
افهم الاكتمال والتسلسل والإلغاء دون افتراض أن الشيفرة غير المتزامنة تعمل بالتوازي.
في هذه الصفحة
أهداف الدرس
- اشرح المفاهيم والمفاضلات المتعلقة بموضوع «الوعود وحلقة الأحداث».
- توقع المثالين وتحقق من مخرجاتهما وسلوك تنظيف الموارد فيهما.
- شخّص الإخفاقات الثلاثة الشائعة وطبق الحلول المشروحة على مدخلات مشابهة.
استدعاء واحد في كل لحظة
تنفذ JavaScript داخل عازل Node عادة استدعاء واحدا في كل لحظة. تستطيع Node انتظار عمليات إدخال وإخراج كثيرة بينما يكون الخيط حرا، ثم تنفذ استدعاءات الاكتمال عند جاهزيتها. يعني التزامن تداخل أعمار الأعمال، ويعني التوازي تنفيذها في الوقت نفسه. لا تنقل الدالة async الحساب تلقائيا إلى معالج آخر. وتظل الحلقة الطويلة قبل أول await مانعة لبقية JavaScript. لذلك قد يخدم الخادم اتصالات منتظرة كثيرة بكفاءة، ثم يتعطل تجاوبه بسبب حساب متزامن كبير في طلب واحد.
الوعد يمثل نتيجة مستقبلية
ينتهي الوعد إما بالنجاح مع قيمة أو بالرفض مع سبب. توقف await الدالة غير المتزامنة الحالية إلى أن تظهر النتيجة، مع إمكان استمرار أعمال أخرى جاهزة. يتيح إرجاع الوعد من دالة مساعدة للمستدعي امتلاك مسؤولية الاكتمال والفشل. يفصل بدء عمل دون إرجاعه أو انتظاره هذه المسؤولية وقد يولّد رفضا غير معالج. ضع try/catch حيث يستطيع التطبيق اتخاذ قرار مفيد. وتناسب finally التنظيف اللازم في الحالتين، لكن ينبغي ألا تخفي أخطاء التنظيف المشكلة الأصلية دون قصد.
ملاحظة الترتيب المضمون
يسجل مثال الترتيب العبارات المتزامنة قبل تفاعل الوعد والمؤقت. ويتجنب مقارنة setTimeout وsetImmediate عند بداية البرنامج، حيث يكون الادعاء بترتيب شامل مضللا. تأخير المؤقت عتبة انتظار وليس حجزا للحظة دقيقة؛ فقد تؤجله JavaScript المشغولة وأعمال أخرى. وتؤثر قوائم المهام الدقيقة أيضا؛ إذ قد يحرم تكرار إضافتها أعمالا تحتاج إلى تقدم الحلقة. تعلّم الضمانات اللازمة لخوارزميتك بدلا من حفظ رسم واحد باعتباره وصفا كاملا لكل بيئة.
import { setTimeout as delay } from 'node:timers/promises';
const order = ['sync'];
Promise.resolve().then(() => order.push('promise'));
setTimeout(() => order.push('timer'), 0);
order.push('after scheduling');
await delay(20);
console.log(order.join(' -> '));
sync -> after scheduling -> promise -> timer
يكتمل العمل المتزامن الحالي قبل تفاعل الوعد ثم يأتي المؤقت. الانتظار 20 ملي ثانية وسيلة للعرض وليس معيار أداء.
بدء الأعمال المستقلة بوعي
يؤدي انتظار عملية قبل بدء التالية إلى تنفيذ متسلسل. ويسمح بدء العمليتين ثم انتظار Promise.all بتداخلهما. تحافظ Promise.all على ترتيب المدخلات في النتيجة مهما اختلف ترتيب الاكتمال، وترفض عند رفض أحد المدخلات، لكنها لا تلغي العمليات المتبقية تلقائيا. استخدم Promise.allSettled عندما تحتاج إلى نتيجة كل عملية. لا تحد أي منهما عدد العمليات المتزامنة؛ لذا قد يستنزف تحويل مليون عنصر إلى وعود الموارد. تناسب المجموعات الصغيرة هذه الطريقة، وتحتاج الأحمال الكبيرة إلى طابور أو عدد محدود من العاملين.
import { setTimeout as delay } from 'node:timers/promises';
const controller = new AbortController();
const waiting = delay(1000, 'ready', { signal: controller.signal });
controller.abort();
try { await waiting; } catch (error) { console.log(error.name); }
const values = await Promise.all([Promise.resolve(4), Promise.resolve(6)]);
console.log(values.join(','));
AbortError
4,6
يرفض الإلغاء وعد المؤقت وينهي انتظاره. وتعيد Promise.all القيم بترتيب تقديمها.
الإلغاء عقد تعاوني
توفر AbortController إشارة، لكن لا تتوقف إلا العمليات التي تراقبها. يستخدم المثال الثاني مؤقتا قائما على الوعود يدعم الإلغاء ويعالج AbortError صراحة. يختلف الإلغاء عن فشل قاعدة عمل؛ فقد يعني أن المستدعي لم يعد يحتاج النتيجة. مرر الإشارة عبر الطبقات الداعمة، وحرر المستمعين والمؤقتات عند الاكتمال. لا يوقف سباق وعد مع مهلة إلا الانتظار؛ وقد تستمر العملية الأساسية ما لم تتلق الإلغاء. صمم الميزانية الزمنية والتنظيف معا كي لا يتراكم العمل المهجور خلف استجابات تبدو سريعة.
الأخطاء الشائعة وإصلاحها
رفض وعد دون معالجة
- لماذا يحدث؟
- بُدئ وعد دون مستدع ينتظره أو يتلقى إرجاعه.
- كيف تصلحه؟
- أرجعه أو انتظره وعالج الفشل عند حد مناسب.
- كيف تتجنب تكراره؟
- راجع كل استدعاء غير متزامن وحدد من يتولى نتيجته.
يتأخر المؤقت
- لماذا يحدث؟
- منع عمل حسابي متزامن تقدم الحلقة.
- كيف تصلحه؟
- قلل الحساب أو قسّمه أو انقله.
- كيف تتجنب تكراره؟
- قس تأخر الحلقة تحت حمل واقعي.
لا توقف المهلة العمل
- لماذا يحدث؟
- أوقفت Promise.race انتظار العملية الأصلية فقط.
- كيف تصلحه؟
- مرر AbortSignal إلى عملية تدعمه.
- كيف تتجنب تكراره؟
- حدد الإلغاء والتنظيف ضمن عقد الدوال المساعدة.
اختبر فهمك
1. تحتاج قراءتان مستقلتان إلى نحو 30 ملي ثانية لكل منهما. اشرح التنفيذ المتسلسل والمتزامن دون الوعد بزمن دقيق.
اعرض الحل المشروح
يبدأ التنفيذ المتسلسل القراءة الثانية بعد انتهاء الأولى فتُجمع فترتا الانتظار. ابدأ القراءتين أولا ثم انتظر Promise.all لتتداخل فترتاهما. قد يقترب الزمن من زمن القراءة الأبطأ، لكن الجدولة والتنافس على الموارد قد يزيدانه.
2. ينقطع اتصال مستدع لواجهة أثناء انتظار طلب خارجي يدعم الإلغاء. ماذا يجب أن يحدث؟
اعرض الحل المشروح
ألغ وحدة تحكم يملكها الطلب ومرر إشارتها إلى الجهة الخارجية، وصنف الإلغاء منفصلا وحرر المستمعين المرتبطين. لا تعد محاولة عمل لم يعد مطلوبا تلقائيا. تحقق باختبار من عدم بقاء كتابة استجابة أو مؤقت خلفي بعد الإلغاء.
أهم ما تعلمته
- تنفذ JavaScript داخل عازل Node عادة استدعاء واحدا في كل لحظة.
- ينتهي الوعد إما بالنجاح مع قيمة أو بالرفض مع سبب.
- توفر AbortController إشارة، لكن لا تتوقف إلا العمليات التي تراقبها.