أنماط ردود النداء وتجنب جحيم ردود النداء
ما هو رد النداء؟
رد النداء (Callback) هو دالة يتم تمريرها كوسيط لدالة أخرى ويتم تنفيذها في وقت لاحق. ردود النداء هي أحد الأنماط الأساسية في جافاسكريبت وهي الأساس للتعامل مع العمليات غير المتزامنة. يأتي اسم "رد النداء" من فكرة أنك تقدم دالة ليتم "استدعاؤها مرة أخرى" عندما يحدث شيء ما -- عندما تصل البيانات من خادم، أو عندما ينتهي مؤقت، أو عندما ينقر المستخدم على زر، أو عندما ينتهي قراءة ملف.
تعمل ردود النداء لأن جافاسكريبت تعامل الدوال كـ مواطنين من الدرجة الأولى. هذا يعني أن الدوال يمكن تعيينها لمتغيرات وتمريرها كوسائط لدوال أخرى وإرجاعها من دوال وتخزينها في هياكل البيانات. هذه القدرة تجعل ردود النداء ممكنة وهي واحدة من أقوى ميزات اللغة.
مثال: رد نداء بسيط
// greet هي دالة رد نداء تمرر إلى performAction
function greet(name) {
console.log('مرحبا، ' + name + '!');
}
function farewell(name) {
console.log('وداعا، ' + name + '!');
}
function performAction(name, actionCallback) {
console.log('تنفيذ إجراء لـ: ' + name);
actionCallback(name); // تنفيذ رد النداء
}
performAction('أحمد', greet); // "تنفيذ إجراء لـ: أحمد" ثم "مرحبا، أحمد!"
performAction('سارة', farewell); // "تنفيذ إجراء لـ: سارة" ثم "وداعا، سارة!"
في هذا المثال، greet وfarewell هما دالتا رد نداء. لا يتم تنفيذهما فورا عند تمريرهما إلى performAction -- يتم تنفيذهما داخل performAction عندما تقرر استدعاءهما. هذا النمط يعطي الدالة المستدعية التحكم في متى وكيف يتم استدعاء رد النداء.
ردود النداء المتزامنة مقابل غير المتزامنة
يمكن أن تكون ردود النداء إما متزامنة أو غير متزامنة. فهم الفرق ضروري لكتابة كود جافاسكريبت صحيح.
ردود النداء المتزامنة تنفذ فورا أثناء تنفيذ الدالة التي مررت إليها. الكود بعد استدعاء الدالة لا يعمل حتى يكتمل رد النداء. توابع المصفوفات مثل forEach وmap وfilter وreduce تستخدم ردود نداء متزامنة.
مثال: ردود النداء المتزامنة (توابع المصفوفات)
const numbers = [1, 2, 3, 4, 5];
// forEach -- رد نداء متزامن، يعمل فورا لكل عنصر
console.log('قبل forEach');
numbers.forEach(function(num) {
console.log('معالجة: ' + num);
});
console.log('بعد forEach');
// ترتيب الإخراج مضمون:
// "قبل forEach"
// "معالجة: 1"
// "معالجة: 2"
// "معالجة: 3"
// "معالجة: 4"
// "معالجة: 5"
// "بعد forEach"
// map -- رد نداء متزامن يحول كل عنصر
const doubled = numbers.map(function(num) {
return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]
// filter -- رد نداء متزامن يختار العناصر
const evens = numbers.filter(function(num) {
return num % 2 === 0;
});
console.log(evens); // [2, 4]
// reduce -- رد نداء متزامن يجمع نتيجة
const sum = numbers.reduce(function(accumulator, num) {
return accumulator + num;
}, 0);
console.log(sum); // 15
ردود النداء غير المتزامنة تنفذ في وقت لاحق، بعد أن ترجع الدالة الحالية. الكود بعد استدعاء الدالة يستمر في العمل بينما العملية غير المتزامنة قيد التنفيذ. دوال المؤقتات (setTimeout وsetInterval) ومستمعو الأحداث وطلبات الشبكة تستخدم ردود نداء غير متزامنة.
مثال: ردود النداء غير المتزامنة
console.log('الخطوة 1: البداية');
// setTimeout -- غير متزامن، رد النداء يعمل بعد التأخير
setTimeout(function() {
console.log('الخطوة 2: هذا يعمل بعد ثانيتين');
}, 2000);
console.log('الخطوة 3: هذا يعمل فورا، قبل الخطوة 2');
// ترتيب الإخراج:
// "الخطوة 1: البداية"
// "الخطوة 3: هذا يعمل فورا، قبل الخطوة 2"
// (بعد ثانيتين)
// "الخطوة 2: هذا يعمل بعد ثانيتين"
اصطلاح رد النداء بالخطأ أولا
في Node.js والعديد من مكتبات جافاسكريبت، تتبع ردود النداء اصطلاح الخطأ أولا (يسمى أيضا "ردود نداء على نمط Node"). تستقبل دالة رد النداء كائن خطأ كوسيطها الأول. إذا نجحت العملية، يكون الخطأ null والنتيجة تمرر كوسيط ثان. إذا فشلت العملية، يحتوي الخطأ على معلومات حول ما حدث خطأ.
مثال: اصطلاح رد النداء بالخطأ أولا
// محاكاة بحث في قاعدة بيانات مع رد نداء بالخطأ أولا
function findUserById(id, callback) {
// محاكاة استعلام قاعدة بيانات غير متزامن
setTimeout(function() {
if (typeof id !== 'number' || id < 1) {
// حالة الخطأ: الاستدعاء بالخطأ كوسيط أول
callback(new Error('معرف مستخدم غير صالح: ' + id), null);
return;
}
// قاعدة بيانات مستخدمين محاكاة
const users = {
1: { id: 1, name: 'أحمد', email: 'ahmed@example.com' },
2: { id: 2, name: 'سارة', email: 'sara@example.com' },
3: { id: 3, name: 'خالد', email: 'khalid@example.com' }
};
const user = users[id];
if (!user) {
// حالة الخطأ: المستخدم غير موجود
callback(new Error('المستخدم غير موجود بالمعرف: ' + id), null);
return;
}
// حالة النجاح: الخطأ null، النتيجة الوسيط الثاني
callback(null, user);
}, 500);
}
// استخدام رد النداء بالخطأ أولا
findUserById(2, function(error, user) {
if (error) {
console.error('خطأ:', error.message);
return;
}
console.log('تم العثور على المستخدم:', user.name); // "تم العثور على المستخدم: سارة"
});
findUserById(99, function(error, user) {
if (error) {
console.error('خطأ:', error.message); // "خطأ: المستخدم غير موجود بالمعرف: 99"
return;
}
console.log('تم العثور على المستخدم:', user.name);
});
null أو undefined عن طريق الخطأ مما سيسبب خطأ وقت التشغيل. نمط الإرجاع المبكر يبقي منطق النجاح نظيفا وغير مزاح.واجهات ردود النداء في العالم الحقيقي
ردود النداء موجودة في كل مكان في جافاسكريبت. إليك أكثر واجهات API شيوعا في العالم الحقيقي التي تستخدم ردود النداء:
مستمعو الأحداث
تابع addEventListener هو أحد أكثر أنماط ردود النداء استخداما في المتصفح. دالة رد النداء (تسمى معالج الأحداث) تنفذ كل مرة يحدث فيها الحدث المحدد على العنصر المستهدف.
مثال: ردود نداء مستمعي الأحداث
// رد نداء حدث النقر
const button = document.getElementById('submit-btn');
button.addEventListener('click', function(event) {
event.preventDefault();
console.log('تم النقر على الزر!');
console.log('موقع النقر: X=' + event.clientX + ', Y=' + event.clientY);
});
// رد نداء حدث الإدخال مع نمط تأخير التنفيذ
let debounceTimer;
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', function(event) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
console.log('البحث عن: ' + event.target.value);
// تنفيذ البحث بعد توقف المستخدم عن الكتابة لـ 300 مللي ثانية
}, 300);
});
// أحداث متعددة على نفس العنصر
const form = document.getElementById('contact-form');
form.addEventListener('submit', function(event) {
event.preventDefault();
console.log('تم إرسال النموذج');
});
form.addEventListener('reset', function() {
console.log('تم إعادة تعيين النموذج');
});
دوال المؤقتات
setTimeout وsetInterval هما دالتان مدمجتان تقبلان ردود نداء لتنفيذها بعد تأخير أو على فترات منتظمة.
مثال: ردود نداء المؤقتات
// setTimeout -- تنفيذ مرة واحدة بعد تأخير
function showNotification(message) {
console.log('إشعار: ' + message);
}
const timerId = setTimeout(showNotification, 3000, 'ستنتهي جلستك قريبا');
// إلغاء المؤقت قبل تنفيذه
// clearTimeout(timerId);
// setInterval -- تنفيذ متكرر على فترات
let count = 0;
const intervalId = setInterval(function() {
count++;
console.log('دقة #' + count);
if (count >= 5) {
clearInterval(intervalId);
console.log('توقف المؤقت بعد 5 دقات');
}
}, 1000);
توابع تكرار المصفوفات
توابع المصفوفات التي تقبل ردود النداء هي من بين أكثر أنماط ردود النداء المتزامنة استخداما. توفر طريقة تصريحية للعمل مع المجموعات.
مثال: ردود نداء توابع المصفوفات
const products = [
{ name: 'حاسوب محمول', price: 999, category: 'إلكترونيات' },
{ name: 'كتاب', price: 15, category: 'تعليم' },
{ name: 'سماعات', price: 79, category: 'إلكترونيات' },
{ name: 'دفتر', price: 5, category: 'تعليم' },
{ name: 'شاشة', price: 349, category: 'إلكترونيات' }
];
// filter -- اختيار المنتجات فوق 50$
const expensive = products.filter(function(product) {
return product.price > 50;
});
console.log(expensive.length); // 3
// map -- استخراج الأسماء فقط
const names = products.map(function(product) {
return product.name;
});
console.log(names); // ["حاسوب محمول", "كتاب", "سماعات", "دفتر", "شاشة"]
// find -- الحصول على أول منتج إلكتروني
const firstElectronic = products.find(function(product) {
return product.category === 'إلكترونيات';
});
console.log(firstElectronic.name); // "حاسوب محمول"
// sort -- ترتيب بالسعر (تصاعدي)
const sorted = [...products].sort(function(a, b) {
return a.price - b.price;
});
// every و some -- اختبار الشروط
const allUnder1000 = products.every(function(product) {
return product.price < 1000;
});
console.log(allUnder1000); // true
const hasEducation = products.some(function(product) {
return product.category === 'تعليم';
});
console.log(hasEducation); // true
جحيم ردود النداء: هرم الموت
عندما يكون لديك عدة عمليات غير متزامنة تعتمد على بعضها البعض، يجب أن تعشش ردود النداء داخل ردود النداء. كل عملية تابعة تضيف مستوى آخر من المسافة البادئة مما ينشئ شكلا يسميه المطورون هرم الموت أو جحيم ردود النداء. هذا النمط يجعل الكود صعبا للغاية في القراءة والصيانة والتتبع والتوسيع.
مثال: جحيم ردود النداء
// سيناريو واقعي: تدفق مصادقة المستخدم
// كل خطوة تعتمد على نتيجة الخطوة السابقة
function authenticateUser(username, password, callback) {
setTimeout(function() {
if (username === 'admin' && password === 'secret') {
callback(null, { userId: 1, token: 'abc123' });
} else {
callback(new Error('بيانات اعتماد غير صالحة'), null);
}
}, 500);
}
function getUserProfile(userId, callback) {
setTimeout(function() {
callback(null, { userId: userId, name: 'المستخدم المدير', role: 'admin' });
}, 500);
}
function getUserPermissions(role, callback) {
setTimeout(function() {
callback(null, ['قراءة', 'كتابة', 'حذف', 'إدارة']);
}, 500);
}
function getNotifications(userId, callback) {
setTimeout(function() {
callback(null, [
{ id: 1, message: 'تعليق جديد على منشورك' },
{ id: 2, message: 'تحديث نظام مجدول' }
]);
}, 500);
}
function logActivity(userId, action, callback) {
setTimeout(function() {
callback(null, { logged: true, timestamp: new Date() });
}, 300);
}
// هرم الموت -- كل رد نداء يعشش داخل السابق
authenticateUser('admin', 'secret', function(err, auth) {
if (err) {
console.error('فشلت المصادقة:', err.message);
return;
}
getUserProfile(auth.userId, function(err, profile) {
if (err) {
console.error('فشل الملف الشخصي:', err.message);
return;
}
getUserPermissions(profile.role, function(err, permissions) {
if (err) {
console.error('فشلت الصلاحيات:', err.message);
return;
}
getNotifications(auth.userId, function(err, notifications) {
if (err) {
console.error('فشلت الإشعارات:', err.message);
return;
}
logActivity(auth.userId, 'login', function(err, logResult) {
if (err) {
console.error('فشل التسجيل:', err.message);
return;
}
// أخيرا لدينا كل ما نحتاجه
console.log('مرحبا،', profile.name);
console.log('الصلاحيات:', permissions);
console.log('الإشعارات:', notifications.length);
console.log('تم تسجيل النشاط في:', logResult.timestamp);
});
});
});
});
});
مشاكل جحيم ردود النداء
جحيم ردود النداء يخلق عدة مشاكل محددة تتجاوز مجرد "المظهر القبيح":
- المقروئية -- التعشيش العميق يجعل التدفق المنطقي للبرنامج صعب المتابعة. لا يمكنك مسح الكود من الأعلى إلى الأسفل وفهم ما يحدث بالترتيب.
- معالجة الأخطاء -- كل رد نداء يجب أن يتحقق من الأخطاء بشكل مستقل. من السهل نسيان فحص خطأ ولا توجد طريقة لالتقاط جميع الأخطاء في مكان واحد. إذا حدث خطأ عميقا في الهرم فإن فك الحالة معقد.
- التتبع -- آثار المكدس في ردود النداء المعششة صعبة التفسير. يجب وضع نقاط توقف في عدة مستويات تعشيش. الطبيعة غير المتزامنة تعني أن مكدس الاستدعاءات لا يظهر السلسلة المنطقية للعمليات.
- عكس التحكم -- عندما تمرر رد نداء لدالة طرف ثالث فإنك تثق في أن تلك الدالة ستستدعي رد النداء بشكل صحيح -- مرة واحدة بالضبط بالوسائط الصحيحة وفي الوقت المناسب. يمكن انتهاك هذه الثقة وليس لديك طريقة لفرضها.
- تكرار الكود -- منطق معالجة الأخطاء يتكرر في كل مستوى تعشيش. إذا أردت إضافة تسجيل أو منطق إعادة المحاولة يجب إضافته في كل مكان.
- صعوبة التركيب -- تشغيل العمليات بالتوازي أو السباق بين العمليات أو تنفيذ الخطوات بشكل مشروط صعب للغاية مع ردود النداء المعششة.
الحل 1: الدوال المسماة
أبسط تحسين هو استخراج ردود النداء المجهولة إلى دوال مسماة. هذا يسطح الهرم ويعطي كل خطوة اسما وصفيا ويجعل الكود أسهل في المتابعة والاختبار بشكل فردي.
مثال: التسطيح بالدوال المسماة
// استخراج كل رد نداء إلى دالة مسماة
function handleAuth(err, auth) {
if (err) {
console.error('فشلت المصادقة:', err.message);
return;
}
// تخزين معلومات المصادقة للاستخدام لاحقا
currentAuth = auth;
getUserProfile(auth.userId, handleProfile);
}
function handleProfile(err, profile) {
if (err) {
console.error('فشل الملف الشخصي:', err.message);
return;
}
currentProfile = profile;
getUserPermissions(profile.role, handlePermissions);
}
function handlePermissions(err, permissions) {
if (err) {
console.error('فشلت الصلاحيات:', err.message);
return;
}
currentPermissions = permissions;
getNotifications(currentAuth.userId, handleNotifications);
}
function handleNotifications(err, notifications) {
if (err) {
console.error('فشلت الإشعارات:', err.message);
return;
}
logActivity(currentAuth.userId, 'login', function(err, logResult) {
if (err) {
console.error('فشل التسجيل:', err.message);
return;
}
displayDashboard(currentProfile, currentPermissions, notifications, logResult);
});
}
function displayDashboard(profile, permissions, notifications, logResult) {
console.log('مرحبا،', profile.name);
console.log('الصلاحيات:', permissions);
console.log('الإشعارات:', notifications.length);
console.log('تم تسجيل النشاط في:', logResult.timestamp);
}
// نقطة دخول نظيفة
let currentAuth, currentProfile, currentPermissions;
authenticateUser('admin', 'secret', handleAuth);
currentAuth وcurrentProfile إلخ) التي يمكن أن تكون مشكلة. لا تحل المشاكل الأساسية في معالجة الأخطاء أو عكس التحكم. الدوال المسماة خطوة أولى جيدة لكن الوعود و async/await توفر حلولا أكثر اكتمالا.الحل 2: الإرجاعات المبكرة وعبارات الحراسة
عندما تحتوي ردود النداء على فحوصات أخطاء متبوعة بمنطق نجاح، استخدم الإرجاعات المبكرة (عبارات الحراسة) للتعامل مع الأخطاء أولا وإبقاء مسار النجاح في أدنى مستوى مسافة بادئة. هذا يقلل التعشيش داخل كل رد نداء.
مثال: نمط الإرجاع المبكر
// بدون إرجاع مبكر -- الخطأ والنجاح بنفس التعشيش
function processFile(filename, callback) {
readFile(filename, function(err, content) {
if (err) {
callback(err, null);
} else {
parseContent(content, function(err, data) {
if (err) {
callback(err, null);
} else {
validateData(data, function(err, result) {
if (err) {
callback(err, null);
} else {
callback(null, result);
}
});
}
});
}
});
}
// مع إرجاعات مبكرة -- أنظف، تعشيش أقل
function processFile(filename, callback) {
readFile(filename, function(err, content) {
if (err) return callback(err, null);
parseContent(content, function(err, data) {
if (err) return callback(err, null);
validateData(data, function(err, result) {
if (err) return callback(err, null);
callback(null, result);
});
});
});
}
الحل 3: التقسيم إلى وحدات
قسم تدفقات ردود النداء المعقدة إلى وحدات أصغر قابلة لإعادة الاستخدام. كل وحدة تتعامل مع جانب واحد من التدفق ويمكن اختبارها بشكل مستقل. هذا النهج يتبع مبدأ المسؤولية الواحدة.
مثال: ردود النداء المقسمة إلى وحدات
// auth.js -- يتعامل مع منطق المصادقة
function loginFlow(username, password, onComplete) {
authenticateUser(username, password, function(err, auth) {
if (err) return onComplete(err, null);
getUserProfile(auth.userId, function(err, profile) {
if (err) return onComplete(err, null);
onComplete(null, { auth: auth, profile: profile });
});
});
}
// permissions.js -- يتعامل مع تحميل الصلاحيات
function loadPermissions(profile, onComplete) {
getUserPermissions(profile.role, function(err, permissions) {
if (err) return onComplete(err, null);
onComplete(null, permissions);
});
}
// notifications.js -- يتعامل مع تحميل الإشعارات
function loadNotifications(userId, onComplete) {
getNotifications(userId, function(err, notifications) {
if (err) return onComplete(err, null);
onComplete(null, notifications);
});
}
// app.js -- يركب الوحدات
function initializeApp(username, password) {
loginFlow(username, password, function(err, loginData) {
if (err) {
console.error('فشل تسجيل الدخول:', err.message);
return;
}
loadPermissions(loginData.profile, function(err, permissions) {
if (err) {
console.error('فشلت الصلاحيات:', err.message);
return;
}
loadNotifications(loginData.auth.userId, function(err, notifications) {
if (err) {
console.error('فشلت الإشعارات:', err.message);
return;
}
console.log('مرحبا،', loginData.profile.name);
console.log('الصلاحيات:', permissions);
console.log('الإشعارات:', notifications.length);
});
});
});
}
initializeApp('admin', 'secret');
الانتقال من ردود النداء إلى الوعود
الوعود توفر نموذجا أفضل جذريا للبرمجة غير المتزامنة. بدلا من تمرير رد نداء إلى دالة، ترجع الدالة كائن وعد (Promise) يمثل الإكمال أو الفشل النهائي للعملية. هذا ينقل التحكم مرة أخرى إلى المستدعي ويمكن من التسلسل ومعالجة الأخطاء المركزية وأنماط التركيب المستحيلة مع ردود النداء العادية.
التحويل إلى وعود يدويا
لتحويل دالة قائمة على ردود النداء إلى دالة قائمة على الوعود، لفها في وعد جديد. استدع resolve عند النجاح وreject عند الفشل.
مثال: لف ردود النداء في وعود
// الدالة الأصلية القائمة على ردود النداء
function findUserById(id, callback) {
setTimeout(function() {
if (id < 1) {
callback(new Error('معرف غير صالح'), null);
return;
}
const users = { 1: { name: 'أحمد' }, 2: { name: 'سارة' } };
const user = users[id];
if (!user) {
callback(new Error('المستخدم غير موجود'), null);
return;
}
callback(null, user);
}, 500);
}
// النسخة المحولة إلى وعد
function findUserByIdPromise(id) {
return new Promise(function(resolve, reject) {
findUserById(id, function(error, user) {
if (error) {
reject(error);
} else {
resolve(user);
}
});
});
}
// الآن يمكننا استخدام سلاسل .then() بدلا من التعشيش
findUserByIdPromise(1)
.then(function(user) {
console.log('تم العثور على:', user.name); // "تم العثور على: أحمد"
})
.catch(function(error) {
console.error('خطأ:', error.message);
});
أداة تحويل عامة إلى وعود
بدلا من لف كل دالة بشكل فردي، يمكنك إنشاء أداة عامة تحول أي دالة بنمط الخطأ أولا إلى دالة ترجع وعدا. هذا هو النمط المستخدم بواسطة util.promisify المدمجة في Node.js.
مثال: بناء أداة تحويل إلى وعود
// دالة تحويل عامة إلى وعود
function promisify(callbackBasedFunction) {
return function(...args) {
return new Promise(function(resolve, reject) {
callbackBasedFunction(...args, function(error, result) {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// تحويل دوال ردود النداء إلى دوال وعود
const findUser = promisify(findUserById);
const getProfile = promisify(getUserProfile);
const getPermissions = promisify(getUserPermissions);
const getNotifs = promisify(getNotifications);
const logAct = promisify(logActivity);
// الآن التدفق بالكامل مسطح وقابل للقراءة
findUser(1)
.then(function(auth) {
return getProfile(auth.userId);
})
.then(function(profile) {
return getPermissions(profile.role);
})
.then(function(permissions) {
console.log('الصلاحيات:', permissions);
})
.catch(function(error) {
// catch واحد يتعامل مع جميع الأخطاء في السلسلة
console.error('فشل شيء ما:', error.message);
});
util.promisify في Node.js
يوفر Node.js دالة util.promisify مدمجة تحول دوال ردود النداء بنمط الخطأ أولا إلى دوال ترجع وعودا. تتعامل مع حالات حدية قد يفوتها تنفيذ مخصص بسيط.
مثال: استخدام util.promisify في Node.js
// تحويل مدمج في Node.js
const util = require('util');
const fs = require('fs');
// تحويل fs.readFile من ردود النداء إلى نسخة الوعود
const readFileAsync = util.promisify(fs.readFile);
// استخدام النسخة المحولة
readFileAsync('./config.json', 'utf8')
.then(function(content) {
const config = JSON.parse(content);
console.log('تم تحميل التكوين:', config);
})
.catch(function(error) {
console.error('فشل قراءة التكوين:', error.message);
});
// أفضل مع async/await
async function loadConfig() {
try {
const content = await readFileAsync('./config.json', 'utf8');
return JSON.parse(content);
} catch (error) {
console.error('خطأ التكوين:', error.message);
return getDefaultConfig();
}
}
// Node.js يوفر أيضا نسخ وعود من العديد من الوحدات المدمجة
// const fs = require('fs').promises; // أو
// const fs = require('fs/promises'); // Node 14+
التحويل الكامل: من ردود النداء إلى async/await
الحل النهائي لجحيم ردود النداء هو async/await الذي يسمح لك بكتابة كود غير متزامن يبدو ويتصرف ككود متزامن. إليك تدفق المصادقة الكامل من مثال جحيم ردود النداء معاد كتابته بالوعود ثم بـ async/await:
مثال: التحويل الكامل
// الخطوة 1: تحويل جميع دوال ردود النداء إلى وعود
const authenticate = promisify(authenticateUser);
const getProfile = promisify(getUserProfile);
const getPerms = promisify(getUserPermissions);
const getNotifs = promisify(getNotifications);
const logAct = promisify(logActivity);
// الخطوة 2: نسخة سلسلة الوعود -- مسطحة، مقروءة، معالج خطأ واحد
function initWithPromises(username, password) {
let authData, profileData;
authenticate(username, password)
.then(function(auth) {
authData = auth;
return getProfile(auth.userId);
})
.then(function(profile) {
profileData = profile;
return getPerms(profile.role);
})
.then(function(permissions) {
return getNotifs(authData.userId).then(function(notifications) {
return { permissions: permissions, notifications: notifications };
});
})
.then(function(data) {
return logAct(authData.userId, 'login').then(function(logResult) {
return {
profile: profileData,
permissions: data.permissions,
notifications: data.notifications,
logResult: logResult
};
});
})
.then(function(result) {
console.log('مرحبا،', result.profile.name);
console.log('الصلاحيات:', result.permissions);
console.log('الإشعارات:', result.notifications.length);
})
.catch(function(error) {
console.error('فشل:', error.message);
});
}
// الخطوة 3: نسخة async/await -- تبدو متزامنة، الأسهل قراءة
async function initWithAsyncAwait(username, password) {
try {
const auth = await authenticate(username, password);
const profile = await getProfile(auth.userId);
const permissions = await getPerms(profile.role);
const notifications = await getNotifs(auth.userId);
const logResult = await logAct(auth.userId, 'login');
console.log('مرحبا،', profile.name);
console.log('الصلاحيات:', permissions);
console.log('الإشعارات:', notifications.length);
console.log('تم تسجيل النشاط في:', logResult.timestamp);
} catch (error) {
console.error('فشل:', error.message);
}
}
// قارن:
// نسخة ردود النداء: ~30 سطرا، 5+ مستويات تعشيش، 5 فحوصات أخطاء
// نسخة الوعود: ~35 سطرا، مسطحة، معالج خطأ واحد
// نسخة async/await: ~15 سطرا، تقرأ من الأعلى للأسفل، try/catch واحد
await متسلسلة ملفوفة في كتلة try/catch.تشغيل عمليات ردود النداء بالتوازي
أحد قيود ردود النداء المتسلسلة أن العمليات المستقلة تعمل واحدة تلو الأخرى مضيعة الوقت. إذا كانت عمليتان لا تعتمدان على بعضهما البعض فيجب أن تعملا بالتزامن. مع ردود النداء يتطلب هذا تنسيقا يدويا. مع الوعود هو بسيط باستخدام Promise.all.
مثال: التنفيذ المتوازي مع ردود النداء مقابل الوعود
// نسخة ردود النداء: تنفيذ متوازٍ يدوي
function loadDashboardData(userId, callback) {
let profile = null;
let notifications = null;
let completedCount = 0;
let hasError = false;
function checkComplete() {
completedCount++;
if (hasError) return;
if (completedCount === 2) {
callback(null, { profile: profile, notifications: notifications });
}
}
getUserProfile(userId, function(err, result) {
if (err) {
hasError = true;
return callback(err, null);
}
profile = result;
checkComplete();
});
getNotifications(userId, function(err, result) {
if (err) {
hasError = true;
return callback(err, null);
}
notifications = result;
checkComplete();
});
}
// نسخة الوعود: نظيفة ومدمجة
async function loadDashboardDataAsync(userId) {
try {
// كلا الطلبين يبدآن بالتزامن
const [profile, notifications] = await Promise.all([
getProfile(userId),
getNotifs(userId)
]);
return { profile, notifications };
} catch (error) {
console.error('فشل تحميل لوحة المعلومات:', error.message);
throw error;
}
}
الاختيار بين ردود النداء والوعود و async/await
لكل نمط مكانه في تطوير جافاسكريبت الحديث. فهم متى تستخدم كل واحد يجعلك مطورا أكثر فعالية.
- استخدم ردود النداء لـ -- معالجات الأحداث البسيطة (
addEventListener)، توابع المصفوفات المتزامنة (mapوfilterوreduce)، دوال المؤقتات لمرة واحدة (setTimeout)، والمواقف التي تعمل فيها مع مكتبة توفر فقط واجهة ردود النداء. - استخدم الوعود لـ -- تركيب عدة عمليات غير متزامنة، تشغيل العمليات بالتوازي مع
Promise.all، السباق بين العمليات معPromise.race، وإنشاء واجهات API عامة سيستهلكها مطورون آخرون. - استخدم async/await لـ -- العمليات غير المتزامنة المتسلسلة حيث تعتمد كل خطوة على السابقة، التحكم المعقد في التدفق مع الشروط والحلقات المتضمنة عمليات غير متزامنة، وأي موقف تكون فيه المقروئية مهمة (وهو تقريبا دائما).
- تجنب -- تعشيش أكثر من مستويين من ردود النداء. إذا وجدت نفسك تعشش ثلاثة أو أكثر من ردود النداء أعد الهيكلة إلى الوعود أو async/await فورا.
مثال: دليل القرار
// ردود النداء -- جيدة للعمليات غير المتزامنة البسيطة لمرة واحدة
document.getElementById('btn').addEventListener('click', handleClick);
setTimeout(showReminder, 5000);
[1, 2, 3].map(function(n) { return n * 2; });
// الوعود -- جيدة للعمليات غير المتزامنة القابلة للتركيب
function fetchData(url) {
return fetch(url).then(function(response) { return response.json(); });
}
// عدة عمليات متوازية
Promise.all([fetchData('/users'), fetchData('/posts')])
.then(function([users, posts]) {
renderPage(users, posts);
});
// ASYNC/AWAIT -- جيد للتسلسل غير المتزامن مع تدفق واضح
async function processOrder(orderId) {
const order = await getOrder(orderId);
const inventory = await checkInventory(order.items);
if (!inventory.available) {
await notifyCustomer(order.customerId, 'نفد المخزون');
return { success: false, reason: 'inventory' };
}
const payment = await processPayment(order.total);
await updateInventory(order.items);
await sendConfirmation(order.customerId, order);
return { success: true, paymentId: payment.id };
}
أنماط مضادة شائعة في ردود النداء
تجنب هذه الأخطاء الشائعة عند العمل مع ردود النداء:
مثال: أنماط مضادة يجب تجنبها
// النمط المضاد 1: عدم معالجة الأخطاء
// سيء -- يتجاهل معامل الخطأ
getData(id, function(err, data) {
console.log(data.name); // ينهار إذا وجد خطأ وكان data يساوي null
});
// جيد -- تحقق دائما من الأخطاء أولا
getData(id, function(err, data) {
if (err) {
console.error('فشل:', err.message);
return;
}
console.log(data.name);
});
// النمط المضاد 2: استدعاء رد النداء عدة مرات
// سيء -- قد يستدعى رد النداء مرتين
function riskyFunction(value, callback) {
if (value < 0) {
callback(new Error('قيمة سالبة'));
// نسي الإرجاع! الكود يستمر في التنفيذ
}
callback(null, value * 2);
}
// جيد -- استخدم return لمنع الاستدعاء المزدوج
function safeFunction(value, callback) {
if (value < 0) {
return callback(new Error('قيمة سالبة'));
}
callback(null, value * 2);
}
// النمط المضاد 3: خلط ردود النداء المتزامنة وغير المتزامنة
// سيء -- أحيانا متزامن، أحيانا غير متزامن (زالغو)
function inconsistentFunction(key, callback) {
if (cache[key]) {
callback(null, cache[key]); // متزامن!
} else {
fetchFromServer(key, callback); // غير متزامن!
}
}
// جيد -- دائما غير متزامن
function consistentFunction(key, callback) {
if (cache[key]) {
setTimeout(function() {
callback(null, cache[key]);
}, 0);
} else {
fetchFromServer(key, callback);
}
}
// النمط المضاد 4: رمي خطأ داخل رد نداء غير متزامن
// سيء -- throw داخل رد نداء غير متزامن غير قابل للالتقاط
getData(id, function(err, data) {
if (err) throw err; // هذا يسقط العملية بأكملها!
processData(data);
});
// جيد -- مرر الأخطاء لرد النداء أو تعامل معها
getData(id, function(err, data) {
if (err) {
console.error('تم التعامل مع الخطأ:', err.message);
return;
}
processData(data);
});
setTimeout(callback, 0) أو queueMicrotask(callback) للنتائج المخزنة مؤقتا.ملخص: تطور جافاسكريبت غير المتزامنة
فهم ردود النداء ضروري حتى في جافاسكريبت الحديثة لأنها تبقى الأساس الذي بنيت عليه الوعود و async/await. إليك مسار التطور:
- ردود النداء (ES1+) -- النمط الأصلي. بسيط وعالمي لكنه يؤدي إلى التعشيش ومشاكل عكس التحكم على نطاق واسع.
- الوعود (ES2015) -- توفر واجهة معيارية للنتائج غير المتزامنة. تمكن التسلسل والتنفيذ المتوازي ومعالجة الأخطاء المركزية. تقضي على جحيم ردود النداء للعمليات المتسلسلة.
- async/await (ES2017) -- سكر نحوي فوق الوعود يجعل الكود غير المتزامن يبدو متزامنا. النهج الأنظف والأكثر مقروئية لمعظم حالات الاستخدام.
كل طبقة تبنى على السابقة. async/await يستخدم الوعود داخليا والوعود في النهاية تتحقق بتنفيذ ردود النداء. معرفة كيف تعمل ردود النداء تعطيك فهما أعمق لنموذج العمليات غير المتزامنة بالكامل وتساعدك في تتبع المشاكل التي تحدث في أي مستوى من التجريد.
تمرين عملي
ابنِ خط أنابيب معالجة ملفات باستخدام جميع الأنماط الثلاثة. أولا، أنشئ أربع دوال قائمة على ردود النداء: readFile(filename, callback) تحاكي قراءة ملف بـ setTimeout وترجع محتوى الملف (استخدم كائنا مبرمجا مسبقا كـ "نظام ملفات")، وparseCSV(content, callback) تقسم المحتوى إلى صفوف وأعمدة، وtransformData(rows, callback) تطبق تحويلا على كل صف (مثل تحويل السلاسل إلى أرقام)، وgenerateReport(data, callback) تنشئ كائن ملخص يحتوي على العدد والمتوسط والإجمالي. ثانيا، اربط جميع الدوال الأربع باستخدام ردود نداء معششة لإنشاء خط الأنابيب الكامل. ثالثا، اكتب دالة أداة promisify واستخدمها لتحويل جميع الدوال الأربع إلى نسخ ترجع وعودا. رابعا، أعد كتابة خط الأنابيب باستخدام سلاسل الوعود مع .then() و.catch() واحد. خامسا، أعد كتابة خط الأنابيب مرة أخرى باستخدام async/await مع try/catch. قارن جميع النسخ الثلاث جنبا إلى جنب. عد الأسطر ومستويات التعشيش وعدد عبارات معالجة الأخطاء. أضف المعالجة المتوازية بإنشاء نسخة تعالج ثلاثة ملفات بالتزامن باستخدام Promise.all. أخيرا، أضف خطأ متعمدا في إحدى الدوال الوسطى وتحقق من أن معالجة الأخطاء تلتقطه بشكل صحيح في جميع النسخ الثلاث.