أساسيات الوعود
الوعود هي واحدة من أهم الميزات في JavaScript الحديثة، حيث أحدثت ثورة في كيفية تعاملنا مع العمليات غير المتزامنة. فهم الوعود ضروري للعمل مع واجهات برمجة التطبيقات، وعمليات الملفات، والمؤقتات، وأي مهمة لا تكتمل على الفور. في هذا الدرس، سنتقن أساسيات الوعود.
ما هي الوعود؟
الوعد هو كائن يمثل الإكمال النهائي أو الفشل لعملية غير متزامنة. إنه بمثابة عنصر نائب لقيمة ستكون متاحة في المستقبل.
المفهوم الأساسي: الوعد يشبه الإيصال الذي تحصل عليه عند طلب الطعام. الإيصال ليس الطعام نفسه، لكنه يعدك بأنك ستحصل على طلبك في النهاية (أو سيتم إخبارك أنه غير متوفر).
حالات الوعد
يمكن أن يكون الوعد في إحدى ثلاث حالات:
1. معلق (Pending): الحالة الأولية، العملية لم تكتمل بعد
2. مُنجز (Fulfilled/Resolved): العملية اكتملت بنجاح
3. مرفوض (Rejected): فشلت العملية
مهم: بمجرد إنجاز أو رفض الوعد، لا يمكنه تغيير حالته.
يُسمى هذا بأنه "مستقر" (settled).
إنشاء وعد
تنشئ وعداً باستخدام مُنشئ Promise، الذي يأخذ دالة منفذة:
const myPromise = new Promise((resolve, reject) => {
// عملية غير متزامنة
const success = true;
if (success) {
resolve("العملية نجحت!"); // إنجاز الوعد
} else {
reject("العملية فشلت!"); // رفض الوعد
}
});
console.log(myPromise); // Promise { <pending> } أو { <fulfilled> }
تتلقى الدالة المنفذة معاملين:
resolve(value) - استدعاء هذا عندما تنجح العملية
reject(reason) - استدعاء هذا عندما تفشل العملية
استهلاك الوعود باستخدام then() و catch()
للتعامل مع نتيجة الوعد، استخدم then() للنجاح و catch() للأخطاء:
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: "أحمد" };
resolve(data); // نجاح!
}, 1000);
});
// التعامل مع النجاح
fetchData
.then((data) => {
console.log("تم استلام البيانات:", data);
// الإخراج: تم استلام البيانات: { id: 1, name: "أحمد" }
})
.catch((error) => {
console.log("خطأ:", error);
});
// مثال مع الرفض
const failingPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("حدث خطأ ما!");
}, 1000);
});
failingPromise
.then((data) => {
console.log("نجاح:", data); // لن يتم تنفيذه
})
.catch((error) => {
console.log("خطأ:", error); // الإخراج: خطأ: حدث خطأ ما!
});
أفضل ممارسة: أضف دائماً catch() للتعامل مع الأخطاء. رفض الوعود غير المعالجة يمكن أن يسبب مشاكل في تطبيقك.
مثال واقعي: محاكاة استدعاء API
لننشئ مثالاً أكثر واقعية يحاكي طلب شبكة:
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
console.log(`جلب البيانات للمستخدم ${userId}...`);
// محاكاة تأخير الشبكة
setTimeout(() => {
// محاكاة النجاح/الفشل
if (userId > 0) {
const userData = {
id: userId,
name: `مستخدم${userId}`,
email: `user${userId}@example.com`
};
resolve(userData);
} else {
reject("معرف مستخدم غير صالح");
}
}, 1500);
});
}
// استخدام الوعد
fetchUserData(123)
.then((user) => {
console.log("تم العثور على المستخدم:", user);
// الإخراج: تم العثور على المستخدم: { id: 123, name: "مستخدم123", ... }
})
.catch((error) => {
console.error("فشل جلب المستخدم:", error);
});
// الاختبار بمعرف غير صالح
fetchUserData(-1)
.then((user) => {
console.log("تم العثور على المستخدم:", user);
})
.catch((error) => {
console.error("فشل جلب المستخدم:", error);
// الإخراج: فشل جلب المستخدم: معرف مستخدم غير صالح
});
سلسلة الوعود
واحدة من أقوى ميزات الوعود هي القدرة على ربطها. كل then() تُرجع وعداً جديداً:
function getUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: "أحمد" });
}, 1000);
});
}
function getUserPosts(user) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, title: "المنشور الأول" },
{ id: 2, title: "المنشور الثاني" }
]);
}, 1000);
});
}
function getPostComments(post) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, text: "منشور رائع!" },
{ id: 2, text: "شكراً للمشاركة" }
]);
}, 1000);
});
}
// ربط الوعود
getUser(1)
.then((user) => {
console.log("المستخدم:", user.name);
return getUserPosts(user); // إرجاع الوعد التالي
})
.then((posts) => {
console.log("المنشورات:", posts.length);
return getPostComments(posts[0]); // إرجاع الوعد التالي
})
.then((comments) => {
console.log("التعليقات:", comments.length);
})
.catch((error) => {
console.error("خطأ في السلسلة:", error);
});
// الإخراج (بعد ~3 ثوانٍ):
// المستخدم: أحمد
// المنشورات: 2
// التعليقات: 2
مهم: أرجع دائماً الوعود في مستدعيات then() إذا كنت تريد ربطها. إذا لم تُرجع، فإن then() التالية ستُنفذ فوراً مع undefined.
تحويل البيانات في السلاسل
يمكنك تحويل البيانات أثناء تدفقها عبر سلسلة الوعود:
function fetchPrice() {
return new Promise((resolve) => {
setTimeout(() => resolve(100), 1000);
});
}
fetchPrice()
.then((price) => {
console.log("السعر الأصلي:", price); // 100
return price * 0.9; // تطبيق خصم 10%
})
.then((discountedPrice) => {
console.log("بعد الخصم:", discountedPrice); // 90
return discountedPrice * 1.15; // إضافة ضريبة 15%
})
.then((finalPrice) => {
console.log("السعر النهائي:", finalPrice); // 103.5
return `$${finalPrice.toFixed(2)}`; // تنسيق
})
.then((formatted) => {
console.log("منسق:", formatted); // $103.50
});
معالجة الأخطاء في سلاسل الوعود
الأخطاء في سلاسل الوعود "تصعد" إلى أقرب catch():
function step1() {
return Promise.resolve("الخطوة 1 اكتملت");
}
function step2() {
return Promise.reject("فشلت الخطوة 2!");
}
function step3() {
return Promise.resolve("الخطوة 3 اكتملت");
}
step1()
.then((result) => {
console.log(result); // الخطوة 1 اكتملت
return step2();
})
.then((result) => {
console.log(result); // لن يتم تنفيذه
return step3();
})
.then((result) => {
console.log(result); // لن يتم تنفيذه
})
.catch((error) => {
console.error("تم اكتشاف خطأ:", error); // تم اكتشاف خطأ: فشلت الخطوة 2!
});
// يمكنك الاستمرار بعد اكتشاف الأخطاء
step1()
.then(() => step2())
.catch((error) => {
console.error("تم التعافي من:", error);
return "قيمة افتراضية"; // التعافي من الخطأ
})
.then((result) => {
console.log("الاستمرار مع:", result); // الاستمرار مع: قيمة افتراضية
});
تحذير: إذا رميت خطأً أو أرجعت وعداً مرفوضاً في catch()، فسينتشر إلى catch() التالية. فقط إرجاع قيمة محللة يتعافى من الخطأ.
طريقة finally()
تعمل طريقة finally() بغض النظر عما إذا كان الوعد قد أُنجز أو رُفض:
function fetchData(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
resolve("تم تحميل البيانات");
} else {
reject("فشل التحميل");
}
}, 1000);
});
}
let isLoading = true;
fetchData(true)
.then((data) => {
console.log("نجاح:", data);
})
.catch((error) => {
console.error("خطأ:", error);
})
.finally(() => {
isLoading = false;
console.log("اكتمل التحميل"); // يعمل دائماً
console.log("isLoading:", isLoading);
});
// حالة استخدام شائعة: عمليات التنظيف
function fetchWithLoading(url) {
showLoadingSpinner();
return fetch(url)
.then(response => response.json())
.then(data => {
displayData(data);
return data;
})
.catch(error => {
showError(error);
throw error;
})
.finally(() => {
hideLoadingSpinner(); // إخفاء العجلة الدوارة دائماً
});
}
إنشاء وعود محللة ومرفوضة
يمكنك إنشاء وعود محللة أو مرفوضة فوراً باستخدام الطرق الثابتة:
// وعد محلول فوراً
const resolvedPromise = Promise.resolve("نجاح فوري!");
resolvedPromise.then((value) => {
console.log(value); // نجاح فوري!
});
// وعد مرفوض فوراً
const rejectedPromise = Promise.reject("فشل فوري!");
rejectedPromise.catch((error) => {
console.error(error); // فشل فوري!
});
// مفيد لتحويل القيم إلى وعود
function getValue(usePromise) {
if (usePromise) {
return Promise.resolve(42);
}
return 42;
}
// الآن يعمل دائماً مع الوعود
Promise.resolve(getValue(false))
.then((value) => {
console.log(value); // 42
});
مثال عملي: عمليات متسلسلة
لنبنِ مثالاً عملياً ينفذ عمليات قاعدة بيانات متسلسلة:
// عمليات قاعدة بيانات محاكاة
function createUser(userData) {
return new Promise((resolve, reject) => {
console.log("إنشاء مستخدم...");
setTimeout(() => {
if (userData.email) {
resolve({ id: Date.now(), ...userData });
} else {
reject("البريد الإلكتروني مطلوب");
}
}, 1000);
});
}
function sendWelcomeEmail(user) {
return new Promise((resolve) => {
console.log(`إرسال بريد ترحيب إلى ${user.email}...`);
setTimeout(() => {
resolve({ user, emailSent: true });
}, 1000);
});
}
function logActivity(data) {
return new Promise((resolve) => {
console.log("تسجيل النشاط...");
setTimeout(() => {
resolve({ ...data, activityLogged: true });
}, 500);
});
}
// تسجيل مستخدم جديد بعمليات متسلسلة
const newUser = {
name: "أحمد علي",
email: "ahmad@example.com"
};
createUser(newUser)
.then((user) => {
console.log("تم إنشاء المستخدم:", user);
return sendWelcomeEmail(user);
})
.then((result) => {
console.log("تم إرسال البريد:", result.emailSent);
return logActivity(result);
})
.then((finalResult) => {
console.log("اكتمل التسجيل:", finalResult);
})
.catch((error) => {
console.error("فشل التسجيل:", error);
})
.finally(() => {
console.log("اكتمل التنظيف");
});
تمرين تطبيقي:
التحدي: أنشئ دالة retryOperation(operation, maxRetries) تحاول تنفيذ دالة تُرجع وعداً حتى maxRetries مرات إذا فشلت.
الحل:
function retryOperation(operation, maxRetries = 3) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
attempts++;
console.log(`محاولة ${attempts}/${maxRetries}`);
operation()
.then(resolve) // نجاح! حل الوعد الرئيسي
.catch((error) => {
if (attempts >= maxRetries) {
// نفدت المحاولات
reject(`فشل بعد ${maxRetries} محاولات: ${error}`);
} else {
// حاول مرة أخرى بعد التأخير
console.log(`إعادة المحاولة...`);
setTimeout(attempt, 1000);
}
});
}
attempt(); // بدء المحاولة الأولى
});
}
// عملية غير موثوقة محاكاة
function unreliableOperation() {
return new Promise((resolve, reject) => {
const success = Math.random() > 0.7; // معدل نجاح 30%
setTimeout(() => {
if (success) {
resolve("نجحت العملية!");
} else {
reject("فشلت العملية");
}
}, 500);
});
}
// اختبار دالة إعادة المحاولة
retryOperation(unreliableOperation, 5)
.then((result) => {
console.log("نجاح:", result);
})
.catch((error) => {
console.error("خطأ نهائي:", error);
});
أنماط الوعود الشائعة
إليك بعض الأنماط المفيدة للعمل مع الوعود:
// 1. غلاف المهلة
function timeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject("انتهت المهلة"), ms);
})
]);
}
// 2. دالة التأخير/النوم
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// الاستخدام
delay(2000).then(() => console.log("مرت ثانيتان"));
// 3. تحويل دالة قائمة على المستدعي إلى وعد
function promisify(callbackFn) {
return function(...args) {
return new Promise((resolve, reject) => {
callbackFn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
}
الملخص
في هذا الدرس، تعلمت:
- الوعود تمثل النتيجة النهائية للعمليات غير المتزامنة
- الوعود لها ثلاث حالات: معلق، منجز، ومرفوض
- إنشاء الوعود باستخدام مُنشئ Promise ودوال resolve/reject
- التعامل مع النتائج باستخدام then() للنجاح و catch() للأخطاء
- ربط الوعود لأداء عمليات متسلسلة
- تحويل البيانات أثناء تدفقها عبر سلاسل الوعود
- استخدام finally() لعمليات التنظيف
- إنشاء وعود محللة/مرفوضة فوراً بطرق ثابتة
- أنماط شائعة: منطق إعادة المحاولة، المهلات، والتحويل إلى وعود
التالي: في الدرس التالي، سنستكشف طرق الوعود مثل Promise.all() و Promise.race() والمزيد للتعامل مع وعود متعددة!