طرق الوعود
توفر JavaScript طرقاً ثابتة قوية للعمل مع وعود متعددة في نفس الوقت. فهم هذه الطرق ضروري لبناء تطبيقات غير متزامنة فعالة. في هذا الدرس، سنستكشف Promise.all() و Promise.race() و Promise.allSettled() و Promise.any()، والأنماط العملية لاستخدامها.
Promise.all() - انتظار جميع الوعود
Promise.all() يأخذ مصفوفة من الوعود ويُرجع وعداً واحداً يُحل عندما تُحل جميع الوعود المدخلة، أو يُرفض إذا رُفض أي وعد.
const promise1 = Promise.resolve(3);
const promise2 = Promise.resolve(42);
const promise3 = Promise.resolve("مرحباً");
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // [3, 42, "مرحباً"]
});
// مثال عملي: جلب مستخدمين متعددين
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: `مستخدم${id}` });
}, Math.random() * 1000);
});
}
Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
])
.then((users) => {
console.log("تم تحميل جميع المستخدمين:", users);
// الإخراج: تم تحميل جميع المستخدمين: [
// { id: 1, name: "مستخدم1" },
// { id: 2, name: "مستخدم2" },
// { id: 3, name: "مستخدم3" }
// ]
})
.catch((error) => {
console.error("فشل تحميل المستخدمين:", error);
});
السلوك الأساسي: Promise.all() يفشل سريعاً - إذا رُفض أي وعد، تُرفض العملية بأكملها فوراً، حتى لو كانت الوعود الأخرى لا تزال معلقة.
Promise.all() مع الرفض
فهم كيفية تعامل Promise.all() مع الفشل أمر بالغ الأهمية:
const promise1 = Promise.resolve("نجاح 1");
const promise2 = Promise.reject("خطأ في الوعد 2");
const promise3 = Promise.resolve("نجاح 3");
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log("نجحت جميعها:", results);
// لن يتم تنفيذ هذا
})
.catch((error) => {
console.error("فشل واحد:", error);
// الإخراج: فشل واحد: خطأ في الوعد 2
// نتائج promise1 و promise3 ضائعة!
});
// مثال واقعي: تحميل موارد الصفحة
function loadCSS() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("تم تحميل CSS"), 500);
});
}
function loadJS() {
return new Promise((resolve, reject) => {
setTimeout(() => reject("فشل تحميل JS"), 300);
});
}
function loadImages() {
return new Promise((resolve) => {
setTimeout(() => resolve("تم تحميل الصور"), 800);
});
}
Promise.all([loadCSS(), loadJS(), loadImages()])
.then((results) => {
console.log("الصفحة جاهزة:", results);
})
.catch((error) => {
console.error("فشل تحميل الصفحة:", error);
// الإخراج: فشل تحميل الصفحة: فشل تحميل JS
});
تحذير: مع Promise.all()، فشل واحد يتسبب في فشل العملية بأكملها. إذا كنت بحاجة للتعامل مع الفشل الجزئي، استخدم Promise.allSettled() بدلاً من ذلك.
Promise.race() - الأول في الإنهاء يفوز
Promise.race() يُرجع وعداً يُحل أو يُرفض بمجرد حل أو رفض أحد الوعود المدخلة:
const promise1 = new Promise((resolve) => {
setTimeout(() => resolve("انتهى الأول"), 500);
});
const promise2 = new Promise((resolve) => {
setTimeout(() => resolve("انتهى الثاني"), 100);
});
const promise3 = new Promise((resolve) => {
setTimeout(() => resolve("انتهى الثالث"), 300);
});
Promise.race([promise1, promise2, promise3])
.then((result) => {
console.log(result); // الإخراج: انتهى الثاني
// الوعود الأخرى تستمر في العمل لكن نتائجها تُتجاهل
});
// مثال عملي: مهلة الطلب
function fetchWithTimeout(url, timeout) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject("انتهت مهلة الطلب"), timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
fetchWithTimeout("https://api.example.com/data", 5000)
.then((response) => response.json())
.then((data) => console.log("البيانات:", data))
.catch((error) => console.error("خطأ:", error));
أنماط Promise.race() الشائعة
إليك حالات استخدام عملية لـ Promise.race():
// 1. نمط المهلة
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(`انتهت المهلة بعد ${ms}ms`), ms);
});
}
function doSomethingAsync() {
return new Promise((resolve) => {
setTimeout(() => resolve("تم"), 3000);
});
}
Promise.race([
doSomethingAsync(),
timeout(2000)
])
.then((result) => console.log(result))
.catch((error) => console.error(error)); // انتهت المهلة بعد 2000ms
// 2. أسرع استجابة خادم
function fetchFromServer1() {
return new Promise((resolve) => {
setTimeout(() => resolve("بيانات الخادم 1"), Math.random() * 1000);
});
}
function fetchFromServer2() {
return new Promise((resolve) => {
setTimeout(() => resolve("بيانات الخادم 2"), Math.random() * 1000);
});
}
function fetchFromServer3() {
return new Promise((resolve) => {
setTimeout(() => resolve("بيانات الخادم 3"), Math.random() * 1000);
});
}
Promise.race([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3()
])
.then((data) => {
console.log("أسرع استجابة:", data);
});
// 3. مهلة تفاعل المستخدم
function waitForUserClick() {
return new Promise((resolve) => {
document.addEventListener('click', () => {
resolve("نقر المستخدم");
}, { once: true });
});
}
Promise.race([
waitForUserClick(),
timeout(10000)
])
.then((result) => console.log(result))
.catch(() => console.log("لا يوجد تفاعل من المستخدم"));
Promise.allSettled() - انتظار الكل، احتفظ بكل النتائج
Promise.allSettled() ينتظر استقرار جميع الوعود (إما الحل أو الرفض) ويُرجع نتائجها:
const promise1 = Promise.resolve("نجاح 1");
const promise2 = Promise.reject("خطأ 2");
const promise3 = Promise.resolve("نجاح 3");
const promise4 = Promise.reject("خطأ 4");
Promise.allSettled([promise1, promise2, promise3, promise4])
.then((results) => {
console.log(results);
// الإخراج: [
// { status: "fulfilled", value: "نجاح 1" },
// { status: "rejected", reason: "خطأ 2" },
// { status: "fulfilled", value: "نجاح 3" },
// { status: "rejected", reason: "خطأ 4" }
// ]
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`الوعد ${index} نجح:`, result.value);
} else {
console.log(`الوعد ${index} فشل:`, result.reason);
}
});
});
أفضل ممارسة: استخدم Promise.allSettled() عندما تحتاج إلى معرفة نتيجة جميع العمليات، بغض النظر عما إذا فشل بعضها. هذا مثالي للعمليات المجمعة حيث النجاح الجزئي مقبول.
مثال عملي: تحميل ملفات دفعية
لنستخدم Promise.allSettled() لسيناريو واقعي:
function uploadFile(file) {
return new Promise((resolve, reject) => {
// محاكاة التحميل مع نجاح/فشل عشوائي
setTimeout(() => {
if (Math.random() > 0.3) {
resolve({ file: file.name, url: `https://cdn.example.com/${file.name}` });
} else {
reject(`فشل تحميل ${file.name}`);
}
}, Math.random() * 2000);
});
}
const files = [
{ name: "مستند.pdf" },
{ name: "صورة1.jpg" },
{ name: "صورة2.jpg" },
{ name: "فيديو.mp4" }
];
const uploadPromises = files.map(file => uploadFile(file));
Promise.allSettled(uploadPromises)
.then((results) => {
const successful = results.filter(r => r.status === "fulfilled");
const failed = results.filter(r => r.status === "rejected");
console.log(`تم التحميل: ${successful.length}/${files.length} ملفات`);
successful.forEach(result => {
console.log("✓ نجاح:", result.value);
});
failed.forEach(result => {
console.error("✗ فشل:", result.reason);
});
// الاستمرار مع التحميلات الناجحة
const uploadedUrls = successful.map(r => r.value.url);
console.log("روابط التحميل:", uploadedUrls);
});
Promise.any() - أول نجاح يفوز
Promise.any() يُحل بمجرد نجاح أي وعد، متجاهلاً الرفض ما لم تُرفض جميع الوعود:
const promise1 = Promise.reject("خطأ 1");
const promise2 = Promise.reject("خطأ 2");
const promise3 = Promise.resolve("نجاح 3");
const promise4 = Promise.resolve("نجاح 4");
Promise.any([promise1, promise2, promise3, promise4])
.then((result) => {
console.log("أول نجاح:", result);
// الإخراج: أول نجاح: نجاح 3
})
.catch((error) => {
console.error("فشلت جميعها:", error);
});
// إذا رُفضت جميع الوعود
Promise.any([
Promise.reject("خطأ 1"),
Promise.reject("خطأ 2"),
Promise.reject("خطأ 3")
])
.then((result) => {
console.log("نجاح:", result);
})
.catch((error) => {
console.error("رُفضت جميعها:", error);
// الإخراج: رُفضت جميعها: AggregateError: تم رفض جميع الوعود
console.log(error.errors); // ["خطأ 1", "خطأ 2", "خطأ 3"]
});
حالات استخدام Promise.any()
سيناريوهات عملية حيث يتفوق Promise.any():
// 1. مصادر بيانات احتياطية
function fetchFromPrimaryAPI() {
return new Promise((resolve, reject) => {
setTimeout(() => reject("API الأساسية معطلة"), 1000);
});
}
function fetchFromSecondaryAPI() {
return new Promise((resolve) => {
setTimeout(() => resolve("بيانات API الثانوية"), 1500);
});
}
function fetchFromCache() {
return new Promise((resolve) => {
setTimeout(() => resolve("بيانات مخزنة مؤقتاً"), 500);
});
}
Promise.any([
fetchFromPrimaryAPI(),
fetchFromSecondaryAPI(),
fetchFromCache()
])
.then((data) => {
console.log("حصلت على البيانات من أسرع مصدر متاح:", data);
// الإخراج: حصلت على البيانات من أسرع مصدر متاح: بيانات مخزنة مؤقتاً
});
// 2. طرق مصادقة متعددة
function authenticateWithEmail(credentials) {
return new Promise((resolve, reject) => {
setTimeout(() => reject("فشلت مصادقة البريد الإلكتروني"), 800);
});
}
function authenticateWithOAuth(provider) {
return new Promise((resolve) => {
setTimeout(() => resolve({ user: "أحمد", method: "OAuth" }), 1200);
});
}
function authenticateWithToken(token) {
return new Promise((resolve, reject) => {
setTimeout(() => reject("رمز غير صالح"), 500);
});
}
Promise.any([
authenticateWithEmail({ email: "user@example.com", password: "pass" }),
authenticateWithOAuth("google"),
authenticateWithToken("abc123")
])
.then((result) => {
console.log("تمت المصادقة:", result);
})
.catch((error) => {
console.error("فشلت جميع طرق المصادقة");
});
المقارنة: اختيار الطريقة المناسبة
إليك دليل سريع لاختيار طريقة الوعد المناسبة:
Promise.all():
✓ استخدم عندما: يجب أن تنجح جميع العمليات
✓ استخدم عندما: تحتاج جميع النتائج معاً
✗ تجنب عندما: النجاح الجزئي مقبول
مثال: تحميل موارد الصفحة الحرجة
Promise.race():
✓ استخدم عندما: تحتاج أسرع نتيجة
✓ استخدم عندما: تنفيذ المهلات
✗ تجنب عندما: تحتاج جميع النتائج
مثال: مهلة الطلب، أسرع استجابة خادم
Promise.allSettled():
✓ استخدم عندما: النجاح الجزئي مقبول
✓ استخدم عندما: تحتاج معرفة جميع النتائج
✓ استخدم عندما: لا يجب أن توقف الإخفاقات العمليات الأخرى
مثال: عمليات مجمعة، تحميلات ملفات متعددة
Promise.any():
✓ استخدم عندما: أي نجاح كافٍ
✓ استخدم عندما: لديك خيارات احتياطية
✗ تجنب عندما: تحتاج جميع النجاحات
مثال: مصادر بيانات متعددة، خوادم زائدة
دمج طرق الوعود
يمكنك دمج طرق الوعود المختلفة لأنماط متقدمة:
// نمط: جلب من مصادر متعددة، مهلة لكل طلب
function fetchWithIndividualTimeouts(urls, timeout) {
const fetchPromises = urls.map(url => {
const fetchPromise = fetch(url).then(r => r.json());
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(`انتهت المهلة: ${url}`), timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
});
return Promise.allSettled(fetchPromises);
}
// نمط: احصل على أسرع استجابة صحيحة مع مهلة
function getFastestValidResponse(urls, timeout) {
const promises = urls.map(url => {
return Promise.race([
fetch(url).then(r => {
if (r.ok) return r.json();
throw new Error(`HTTP ${r.status}`);
}),
new Promise((_, reject) => {
setTimeout(() => reject("انتهت المهلة"), timeout);
})
]);
});
return Promise.any(promises);
}
// الاستخدام
const apiUrls = [
"https://api1.example.com/data",
"https://api2.example.com/data",
"https://api3.example.com/data"
];
getFastestValidResponse(apiUrls, 3000)
.then(data => console.log("حصلت على البيانات:", data))
.catch(error => console.error("فشلت جميع APIs:", error));
أفضل ممارسات معالجة الأخطاء
معالجة الأخطاء بشكل صحيح مع طرق الوعود:
// ❌ سيئ: لا توجد معالجة أخطاء
Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
// ✅ جيد: معالجة الأخطاء دائماً
Promise.all([fetch(url1), fetch(url2), fetch(url3)])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(data => console.log(data))
.catch(error => console.error("فشل:", error));
// ✅ أفضل: معالجة أخطاء فردية مع allSettled
const promises = [url1, url2, url3].map(url =>
fetch(url)
.then(r => r.json())
.catch(error => ({ error: error.message, url }))
);
Promise.allSettled(promises)
.then(results => {
const successful = results
.filter(r => r.status === "fulfilled" && !r.value.error)
.map(r => r.value);
const failed = results
.filter(r => r.status === "rejected" || r.value.error);
console.log(`نجاح: ${successful.length}، فشل: ${failed.length}`);
});
تمرين تطبيقي:
التحدي: أنشئ دالة fetchMultipleWithRetry(urls, maxRetries) تجلب من روابط متعددة، وتعيد محاولة الطلبات الفاشلة حتى maxRetries مرات، وتُرجع جميع النتائج باستخدام Promise.allSettled().
الحل:
function fetchWithRetry(url, maxRetries = 3) {
function attempt(retriesLeft) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(error => {
if (retriesLeft <= 0) {
throw new Error(`فشل بعد ${maxRetries} محاولات: ${error.message}`);
}
console.log(`إعادة محاولة ${url}... (${retriesLeft} محاولات متبقية)`);
return new Promise(resolve => setTimeout(resolve, 1000))
.then(() => attempt(retriesLeft - 1));
});
}
return attempt(maxRetries);
}
function fetchMultipleWithRetry(urls, maxRetries = 3) {
const fetchPromises = urls.map(url => {
return fetchWithRetry(url, maxRetries)
.then(data => ({ url, success: true, data }))
.catch(error => ({ url, success: false, error: error.message }));
});
return Promise.allSettled(fetchPromises)
.then(results => {
return results.map(result => result.value);
});
}
// الاستخدام
const urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments"
];
fetchMultipleWithRetry(urls, 3)
.then(results => {
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`ناجح: ${successful.length}/${urls.length}`);
console.log("النتائج:", results);
});
اعتبارات الأداء
نصائح لتحسين أداء الوعود:
// ❌ تنفيذ متسلسل (بطيء)
async function loadDataSequential() {
const data1 = await fetch(url1);
const data2 = await fetch(url2);
const data3 = await fetch(url3);
return [data1, data2, data3];
}
// ✅ تنفيذ متوازي (سريع)
async function loadDataParallel() {
return Promise.all([
fetch(url1),
fetch(url2),
fetch(url3)
]);
}
// الحد من العمليات المتزامنة
function limitConcurrency(promises, limit) {
const results = [];
const executing = [];
for (const promise of promises) {
const p = Promise.resolve(promise).then(result => {
executing.splice(executing.indexOf(p), 1);
return result;
});
results.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
}
executing.push(p);
}
return Promise.all(results);
}
الملخص
في هذا الدرس، تعلمت:
Promise.all() ينتظر نجاح جميع الوعود أو يفشل سريعاً
Promise.race() يُرجع أول وعد يستقر (يُحل أو يُرفض)
Promise.allSettled() ينتظر جميع الوعود ويُرجع جميع النتائج
Promise.any() يُرجع أول وعد ناجح
- كيفية اختيار طريقة الوعد المناسبة لحالة الاستخدام الخاصة بك
- دمج طرق الوعود لأنماط متقدمة
- أفضل ممارسات معالجة الأخطاء للوعود المتعددة
- تحسين الأداء بالتنفيذ المتوازي
- أنماط عملية: المهلات، إعادة المحاولات، الاحتياطيات، العمليات المجمعة
التالي: في الدرس التالي، سنتقن async/await - البناء الجملة الحديث لكتابة كود غير متزامن يبدو متزامناً!