1.2غير المتزامن: رد الاتصال (Callbacks) والوعود (Promises) وasync/await
الأساسي في 30 ثانية
- غير المتزامن = «عندما يجهز»: العمليات البطيئة تنطلق ونتيجتها تصل لاحقًا — ولا يمكن قراءتها في السطر التالي.
- رد الاتصال ← جحيم رد الاتصال: اتفاقية error-first (الخطأ في الوسيط الأول) تعمل، لكن دوال رد الاتصال التابعة تتداخل في هرم غير مقروء.
- يُفضَّل async/await: await تنتظر دون حظر، والكود يُقرأ كالمتزامن، والأخطاء عبر try/catch — مبنية على الوعود (Promises).
حالة مستمرة — ساليم ينتظر ملفاته في الجزائر
يجب على ساليم قراءة ملف إعدادات، ثم استجواب قاعدة بيانات بمحتواه، ثم استدعاء API: كتب ثلاث دوال رد اتصال متداخلة وتاه في المسافات البادئة — والأسوأ أن console.log لديه يعرض undefined لأنه يقرأ النتيجة قبل وصولها. أعاد كتابة الكل بـ async/await مع try/catch: ثلاثة أسطر await مقروءة بالترتيب، والخطأ مُدار في مكان واحد. اختفى خطأ الـ undefined مع النموذج الذهني القديم.
تكملة الحالة 1.1 — ينتقل ساليم من اللغة إلى قلب Node: غير المتزامن.
1. من رد الاتصال إلى جحيم رد الاتصال
غير المتزامن هو المفهوم المركزي في Node: العمليات البطيئة (قراءة ملف، استجواب قاعدة، استدعاء API) تنطلق ونتيجتها تصل لاحقًا. الآلية التاريخية هي دالة رد الاتصال (Callback): دالة تُمرَّر كوسيط، تُستدعى عند نهاية العملية — مع اتفاقية error-first (الخطأ في الوسيط الأول، null إذا سار كل شيء بخير). تسلسل عمليات تابعة يقود إلى جحيم رد الاتصال (Callback Hell): هرم غير مقروء، صعب الإدارة خاصة للأخطاء.
يكتب ساليم fs.readFile('config.txt', (err, data) => ...) ويلاحظ اتفاقية error-first: اختبار err أولًا، ثم استغلال data — وليس العكس أبدًا.
2. الوعود (Promises): القيمة المستقبلية المسلسلة
الوعد (Promise) يمثل قيمة مستقبلية — نتيجة عملية لم تنته بعد. ثلاث حالات: pending (انتظار)، fulfilled (نجاح، قيمة)، rejected (خطأ). نتفاعل مع .then() (نجاح) و.catch() (خطأ)، والوعود تتسلسل بدل التداخل — أكثر مقروئية بكثير من جحيم رد الاتصال.
يعيد ساليم كتابة قراءة الملف بـ fs.promises.readFile(...).then().catch(): السلسلة تحل محل الهرم والخطأ يستقر في .catch() واحد.
3. async/await: المقروئية (يُفضَّل)
الثورة الحقيقية في المقروئية هي async/await: داخل دالة async، await تنتظر النتيجة دون حظر (سكر نحوي فوق الوعود) والكود غير المتزامن يُقرأ كالمتزامن، سطرًا بعد سطر، مع try/catch كلاسيكية للأخطاء. هذا هو الأسلوب السائد والموصى به: ستصادفون الأشكال الثلاثة في الكود الموجود، لكنكم ستكتبون أساسًا بـ async/await.
ينهي ساليم مع const data = await fs.promises.readFile(...) داخل try/catch: ثلاث عمليات في ثلاثة أسطر مرتبة — صار كوده غير المتزامن يُقرأ أخيرًا كوصفة.
الهرم الجهنمي (لا تفعلوا هذا أبدًا)
العبرة: التداخل دفن للنفس — والتسطيح مع await تنفُّس.
🔎 مثال ملموس — 🔎 مثال عملي — الكود نفسه بـ async/await
مقروء، غير حاظر، أخطاء مُدارة — الأسلوب الذي يجب كتابته.
const fs = require("fs");
async function lire() {
try {
const data = await fs.promises.readFile("f.txt", "utf8");
console.log(data);
} catch (err) {
console.error("Echec :", err.message);
}
}
lire();
القراءة: await تنتظر المحتوى دون حظر؛ وعند الفشل يعرض catch الرسالة.
الدرس: Promise.all لإطلاق عدة عمليات مستقلة بالتوازي.
مفردات القسم
- دالة رد الاتصال (Callback)
- دالة تُمرَّر كوسيط، تُستدعى عندما تنتهي عملية غير متزامنة. اتفاقية Node error-first: الوسيط الأول = الخطأ (null إذا سار كل شيء بخير)، ثم النتيجة.
مثال بسيط: الـ (err, data) => ... الخاصة بقراءة ملف ساليم.
ما ليس كذلك: دالة تُستدعى فورًا بنتيجة جاهزة مسبقًا.
رد فعل: اختبر err أولًا، ثم استغل النتيجة.
- جحيم رد الاتصال (Callback Hell)
- تداخل غير مقروء لدوال رد الاتصال عند تسلسل عمليات غير متزامنة تابعة؛ مشكلة حلّتها الوعود وasync/await.
مثال بسيط: الهرم ذو المستويات الثلاثة في سكربت ساليم الأول.
ما ليس كذلك: ثلاث await مصطفّة بمقروئية داخل دالة async.
رد فعل: متى تداخل، انتقل إلى الوعود.
- الوعد (Promise)
- كائن يمثل قيمة مستقبلية. ثلاث حالات: pending وfulfilled (نجاح) وrejected (خطأ). التفاعل عبر .then() و.catch().
مثال بسيط: الوعد المُرجَع من fs.promises.readFile لدى ساليم.
ما ليس كذلك: محتوى الملف متاح في السطر التالي.
رد فعل: .then للنجاح، و.catch للخطأ.
- async/await
- صياغة حديثة مبنية على الوعود: await تنتظر النتيجة دون حظر؛ والكود غير المتزامن يُقرأ كالمتزامن. الأخطاء عبر try/catch.
مثال بسيط: الـ await الثلاث المرتبة في سكربت ساليم النهائي.
ما ليس كذلك: sleep تحظر التطبيق كله.
رد فعل: اكتب بـ async/await افتراضيًا.
- error-first / try-catch
- إدارة الأخطاء حسب الشكل: الخطأ في الوسيط الأول لدوال رد الاتصال، و.catch() للوعود، وtry/catch حول await.
مثال بسيط: الـ try/catch الوحيدة التي تؤمّن عمليات ساليم الثلاث.
ما ليس كذلك: تجاهل الخطأ وترك التطبيق يتعطل بصمت.
رد فعل: دائمًا مسار خطأ صريح.
في التطبيق — عمليًا — إتقان غير المتزامن
- اكتبوا عملية غير متزامنة بدالة رد اتصال (مثل fs.readFile) ولاحظوا اتفاقية error-first (err, data).
- سلسِلوا عمليتين تابعتين بدالتي رد اتصال متداخلتين: لاحظوا بداية جحيم رد الاتصال.
- أعيدوا الكتابة بوعود وتسلسل .then().then().catch(): لاحظوا المقروئية الأفضل.
- أعيدوا الكتابة أخيرًا بـ async/await (دالة async، وawait أمام العمليات، وtry/catch للأخطاء): الكود يُقرأ كالمتزامن.
نص جاهز — شرح غير المتزامن لزميل (دقيقة)
«في Node، العملية البطيئة تنطلق لكن نتيجتها تصل لاحقًا: لا تُقرأ أبدًا في السطر التالي. نقول +عندما يجهز، افعل هذا+ — بدالة رد اتصال أو .then() أو await. لهذا نكتب بـ async/await: الكود ينتظر دون حظر ويُقرأ بالترتيب.»
يُقال أمام الخطأ الكلاسيكي لـ console.log الذي يعرض undefined.
نموذج نسخ ولصق — قراءة ملف بـ async/await
const fs = require("fs");
async function lire(nom) {
try {
const data = await fs.promises.readFile(nom, "utf8");
console.log(data);
return data;
} catch (err) {
console.error("Echec :", err.message);
return null;
}
}
lire("f.txt");
عمليات تابعة
قراءة ملف، ثم استجواب بمحتواه: سلسِلوا await بالترتيب داخل try/catch واحدة — كما فعل ساليم مع config ← base ← API.
عمليات مستقلة
قراءات متعددة لا رابط بينها: أطلقوها معًا مع Promise.all للسرعة بدل انتظار كل واحدة دورها.
كود قديم بدوال رد اتصال
أمام API error-first موجودة: افهموها واستعملوها كما هي للقراءة، لكن اكتبوا كل الكود الجديد بـ async/await.
خطأ صامت في الجزائر
يضيف ساليم منهجيًا catch تعرض err.message: خطأ غير متزامن غير مُدار يعطّل أو يكذب بصمت — لا await دون شبكة أمان أبدًا.
✓ ممارسات جيدة
- ميّزوا بين «الإطلاق» و«الحصول».
- اكتبوا بـ async/await افتراضيًا.
- دائمًا مسار خطأ صريح.
✗ أخطاء شائعة
- قراءة النتيجة في السطر التالي.
- تداخل دوال رد الاتصال التابعة.
- await دون try/catch.
النقاط الأساسية
- غير المتزامن هو المفهوم المركزي في Node: العمليات البطيئة تنطلق ونتيجتها تصل لاحقًا.
- التطور: دوال رد الاتصال (error-first) ← خطر جحيم رد الاتصال ← الوعود (تسلسل .then/.catch) ← ASYNC/AWAIT.
- الوعد = قيمة مستقبلية (pending/fulfilled/rejected)؛ نتفاعل مع .then() (نجاح) و.catch() (خطأ).
- ASYNC/AWAIT (يُفضَّل): await تنتظر دون حظر، والكود يُقرأ كالمتزامن، والأخطاء عبر try/catch.
تحدي 48 ساعة — طبّق غدًا
خذوا النموذج أعلاه: اقرأوا ملفًا حقيقيًا بـ دالة رد اتصال، وأعيدوا الكتابة بـ الوعود، ثم بـ async/await مع try/catch. قارنوا النسخ الثلاث. تكفي 25 دقيقة.
- أستطيع شرح «الإطلاق» مقابل «الحصول على النتيجة».
- أعرف متى أستعمل رد الاتصال أو الوعد أو await.
- كتبت العملية نفسها بالأشكال الثلاثة.
أسئلة شائعة
لماذا يكون غير المتزامن مربكًا جدًا في البداية، وكيف نفهمه حقًا؟
يكسر الحدس «كل سطر ينتهي قبل التالي». الفخ الكلاسيكي: قراءة النتيجة في السطر التالي للاستدعاء — نحصل على undefined لأن العملية لم تنته بعد. النموذج الذهني الواجب تبنيه: التمييز بين «الإطلاق» (فوري) و«الحصول على النتيجة» (لاحقًا، عبر دالة رد اتصال أو .then() أو await). فكّروا «عندما يجهز، افعل هذا» — كطلب في مطعم: لا نبقى واقفين في المطبخ، بل نواصل ونُخدَم عندما يجهز. وما إن يحدث هذا الانفتاح، كما حدث لساليم، يصير كل Node مقروءًا.
دوال رد الاتصال أم الوعود أم async/await: أيها نستعمل اليوم؟
يجب أن تعرفوا الثلاثة لكن تكتبوا بـ async/await. دوال رد الاتصال: الشكل التاريخي، للفهم لقراءة الكود الموجود والـ API الكلاسيكية (اتفاقية error-first) — تجنّبوا تداخلها. الوعود: الأساس الحديث (async/await مبنية عليها) — للفهم، ومفيدة مباشرة للتوازي (Promise.all). async/await: أسلوبكم الافتراضي لكل كود جديد — مقروء، والأخطاء عبر try/catch. قاعدة عملية: اكتبوا بـ await، وافهموا الوعود، واعرفوا قراءة دوال رد الاتصال.
ماذا تفعل await داخل دالة async؟