JavaScript المتقدم (ES6+)

طرق الوعود

13 دقيقة الدرس 15 من 40

طرق الوعود

توفر 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 - البناء الجملة الحديث لكتابة كود غير متزامن يبدو متزامناً!