أساسيات JavaScript

الوعود وتسلسل الوعود

45 دقيقة الدرس 40 من 60

مشكلة الاستدعاءات الراجعة

قبل وجود الوعود في JavaScript، كانت العمليات غير المتزامنة تُعالج حصريًا من خلال الاستدعاءات الراجعة (callbacks) -- دوال تُمرر كوسائط لتُنفذ عند اكتمال العملية. بينما تعمل الاستدعاءات الراجعة في الحالات البسيطة، فإنها تصبح غير قابلة للإدارة بسرعة عندما تحتاج إلى تنفيذ عمليات غير متزامنة متعددة بالتسلسل. يجب أن تكون كل عملية لاحقة متداخلة داخل الاستدعاء الراجع للعملية السابقة، مما ينشئ بنية عميقة المسافة البادئة وصعبة القراءة تُعرف باسم جحيم الاستدعاءات الراجعة (وتسمى أيضًا "هرم الهلاك").

مثال: جحيم الاستدعاءات الراجعة

// محاكاة استدعاءات API بالاستدعاءات الراجعة
function getUser(userId, callback) {
    setTimeout(() => {
        callback(null, { id: userId, name: 'Alice', teamId: 5 });
    }, 1000);
}

function getTeam(teamId, callback) {
    setTimeout(() => {
        callback(null, { id: teamId, name: 'Engineering', companyId: 3 });
    }, 1000);
}

function getCompany(companyId, callback) {
    setTimeout(() => {
        callback(null, { id: companyId, name: 'TechCorp' });
    }, 1000);
}

// هرم الهلاك -- كل استدعاء متداخل داخل السابق
getUser(1, function(err, user) {
    if (err) {
        console.error('فشل في الحصول على المستخدم:', err);
        return;
    }
    getTeam(user.teamId, function(err, team) {
        if (err) {
            console.error('فشل في الحصول على الفريق:', err);
            return;
        }
        getCompany(team.companyId, function(err, company) {
            if (err) {
                console.error('فشل في الحصول على الشركة:', err);
                return;
            }
            console.log(`${user.name} يعمل في ${company.name}`);
            // تخيل إضافة المزيد من الاستدعاءات المتداخلة هنا...
        });
    });
});

مشاكل جحيم الاستدعاءات الراجعة تتجاوز المظهر. تصبح معالجة الأخطاء متكررة ومعرضة للخطأ لأنك يجب أن تتحقق من الأخطاء في كل مستوى. تدفق التحكم يصعب التفكير فيه. إضافة خطوات جديدة في منتصف السلسلة يتطلب إعادة هيكلة التداخل بالكامل. وإرجاع قيمة من الاستدعاءات الراجعة المتداخلة بعمق إلى النطاق الخارجي شبه مستحيل عمليًا. تم إنشاء الوعود لحل كل هذه المشاكل.

ملاحظة: الاستدعاءات الراجعة ليست سيئة بطبيعتها. تعمل بشكل جيد لمعالجي الأحداث البسيطة (مثل مستمعي النقر) والعمليات غير المتزامنة المفردة. تنشأ المشكلة تحديدًا عندما تحتاج إلى تنسيق عمليات غير متزامنة متعددة بالتسلسل أو بالتوازي.

مفهوم الوعد

الوعد (Promise) هو كائن يمثل الإتمام النهائي (أو الفشل) لعملية غير متزامنة وقيمتها الناتجة. فكر فيه كعنصر نائب لقيمة لا توجد بعد ولكنها ستوجد في نقطة ما في المستقبل. الوعد دائمًا في إحدى ثلاث حالات:

  • قيد الانتظار (Pending) -- الحالة الأولية. لم تكتمل العملية بعد.
  • مُنجز (Fulfilled) -- اكتملت العملية بنجاح، والوعد لديه قيمة ناتجة.
  • مرفوض (Rejected) -- فشلت العملية، والوعد لديه سبب للفشل (خطأ).

بمجرد أن يصبح الوعد مُنجزًا أو مرفوضًا، يقال أنه مُستقر، ولا يمكن أن تتغير حالته أبدًا مرة أخرى. الوعد المُنجز لن يصبح مرفوضًا، والعكس صحيح. هذه الثبات هي إحدى نقاط القوة الرئيسية للوعود -- بمجرد أن تحصل على نتيجة، يمكنك الوثوق بها.

نصيحة احترافية: تشبيه مفيد هو طلب مطعم. عندما تقدم طلبًا، تحصل على إيصال (الوعد). الإيصال ليس الطعام نفسه، ولكنه ضمان بأن الطعام سيأتي. الطلب "قيد الانتظار" بينما يعمل المطبخ. يصبح "مُنجزًا" عندما يصل طعامك، أو "مرفوضًا" إذا لم يتمكن المطبخ من إعداد الطبق. في كلتا الحالتين، يمنحك الإيصال طريقة للانتظار والتفاعل مع النتيجة.

إنشاء الوعود

يمكنك إنشاء وعد جديد باستخدام المُنشئ new Promise(). يأخذ المُنشئ وسيطًا واحدًا: دالة تسمى المُنفِّذ (executor). يتلقى المُنفِّذ وسيطين: resolve و reject. استدعِ resolve(value) عندما تنجح العملية، واستدعِ reject(reason) عندما تفشل.

مثال: إنشاء وعد

// إنشاء وعد أساسي
const myPromise = new Promise((resolve, reject) => {
    // محاكاة عملية غير متزامنة
    const success = true;

    setTimeout(() => {
        if (success) {
            resolve('اكتملت العملية بنجاح!');
        } else {
            reject(new Error('فشلت العملية.'));
        }
    }, 1000);
});

console.log(myPromise); // Promise { <pending> }
// بعد ثانية واحدة، سيصبح الوعد مُنجزًا

مثال: تغليف واجهة API قائمة على الاستدعاءات الراجعة في وعد

// تحويل دالة استدعاء راجع لتُرجع وعدًا
function getUserPromise(userId) {
    return new Promise((resolve, reject) => {
        // محاكاة استدعاء قاعدة بيانات
        setTimeout(() => {
            if (userId <= 0) {
                reject(new Error('معرف مستخدم غير صالح'));
                return;
            }

            resolve({
                id: userId,
                name: 'Alice',
                email: 'alice@example.com'
            });
        }, 1000);
    });
}

// الآن يمكننا استخدامها مع .then() و .catch()
getUserPromise(1);  // تُرجع وعدًا
getUserPromise(-1); // تُرجع وعدًا سيُرفض
مهم: دالة المُنفِّذ تعمل فورًا عند إنشاء الوعد. إنها ليست مؤجلة. فقط استدعاءات resolve و reject (ومعالجات .then() و .catch() اللاحقة) هي غير متزامنة. أيضًا، استدعاء resolve() أو reject() أكثر من مرة ليس له أي تأثير -- فقط الاستدعاء الأول هو المهم. يمكن أن يُستقر الوعد مرة واحدة فقط.

معالج .then()

التابع .then() هو كيفية الوصول إلى قيمة الوعد المُنجز. يأخذ ما يصل إلى وسيطين: استدعاء راجع لحالة النجاح واستدعاء راجع اختياري لحالة الرفض. التابع .then() نفسه يُرجع وعدًا جديدًا، وهو أساس تسلسل الوعود.

مثال: استخدام .then()

const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve(42), 1000);
});

// استخدام .then() الأساسي
promise.then((value) => {
    console.log('الإجابة هي:', value); // "الإجابة هي: 42"
});

// .then() مع كلا المعالجين
promise.then(
    (value) => {
        console.log('نجاح:', value);
    },
    (error) => {
        console.log('خطأ:', error.message);
    }
);

// .then() تُرجع دائمًا وعدًا جديدًا
const newPromise = promise.then((value) => {
    return value * 2;
});

newPromise.then((doubled) => {
    console.log('المضاعف:', doubled); // "المضاعف: 84"
});

القيمة المُرجعة من استدعاء .then() الراجع تحدد قيمة الوعد المُرجع. إذا أرجعت قيمة عادية، يتلقى .then() التالي تلك القيمة. إذا أرجعت وعدًا، تنتظر السلسلة حتى يستقر ذلك الوعد قبل المتابعة. إذا طرحت خطأ، يصبح الوعد المُرجع مرفوضًا.

مثال: القيم المُرجعة في .then()

Promise.resolve(10)
    .then((value) => {
        console.log(value);   // 10
        return value + 5;     // إرجاع قيمة
    })
    .then((value) => {
        console.log(value);   // 15
        return new Promise((resolve) => {
            setTimeout(() => resolve(value * 2), 500);
        });                    // إرجاع وعد
    })
    .then((value) => {
        console.log(value);   // 30 (انتظر الوعد الداخلي)
        // لا إرجاع -- يُرجع undefined ضمنيًا
    })
    .then((value) => {
        console.log(value);   // undefined
    });

معالج .catch()

يتعامل التابع .catch() مع الوعود المرفوضة. وهو مكافئ لاستدعاء .then(undefined, onRejected)، لكنه أكثر قراءة وله ميزة مهمة واحدة: يلتقط الأخطاء المطروحة في أي مكان في السلسلة السابقة، وليس فقط في .then() السابق مباشرة.

مثال: استخدام .catch()

// التقاط وعد مرفوض
const failedPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(new Error('انتهت مهلة الشبكة'));
    }, 1000);
});

failedPromise.catch((error) => {
    console.error('تم الالتقاط:', error.message);
    // "تم الالتقاط: انتهت مهلة الشبكة"
});

// .catch() يلتقط الأخطاء من أي مكان في السلسلة
Promise.resolve('بداية')
    .then((value) => {
        console.log(value);  // "بداية"
        throw new Error('حدث خطأ ما');
    })
    .then((value) => {
        // يتم تخطي هذا لأن .then() السابق طرح خطأ
        console.log('هذا لا يعمل أبدًا');
    })
    .catch((error) => {
        console.error(error.message); // "حدث خطأ ما"
        return 'تم الاسترداد'; // يمكنك الاسترداد من الأخطاء!
    })
    .then((value) => {
        console.log(value); // "تم الاسترداد" -- تستمر السلسلة
    });
نصيحة احترافية: استخدم دائمًا .catch() في نهاية سلاسل الوعود بدلاً من تمرير وسيط ثانٍ إلى .then(). نهج .catch() يلتقط الأخطاء من جميع معالجات .then() السابقة، بينما معالج الرفض في .then() يلتقط فقط الرفض من الوعد المرتبط به مباشرة، وليس من معالج النجاح في نفس استدعاء .then().

معالج .finally()

يُشغّل التابع .finally() استدعاءً راجعًا عندما يستقر الوعد، بغض النظر عما إذا تم إنجازه أو رفضه. إنه مفيد لعمليات التنظيف مثل إخفاء مؤشر التحميل، أو إغلاق اتصال قاعدة بيانات، أو تحرير الموارد. لا يتلقى الاستدعاء الراجع أي وسائط ويتم تجاهل القيمة المُرجعة (ما لم تطرح خطأ أو تُرجع وعدًا مرفوضًا).

مثال: استخدام .finally()

function fetchData(url) {
    console.log('جاري التحميل...');
    showLoadingSpinner();

    return fetch(url)
        .then((response) => {
            if (!response.ok) {
                throw new Error(`خطأ HTTP! الحالة: ${response.status}`);
            }
            return response.json();
        })
        .then((data) => {
            console.log('تم استلام البيانات:', data);
            return data;
        })
        .catch((error) => {
            console.error('فشل الجلب:', error.message);
            return null; // إرجاع قيمة بديلة
        })
        .finally(() => {
            // يعمل سواء نجح الجلب أو فشل
            hideLoadingSpinner();
            console.log('اكتمل التحميل.');
        });
}

// .finally() لا يغير القيمة المحلولة
Promise.resolve(42)
    .finally(() => {
        console.log('تنظيف'); // يعمل
        return 999;             // يتم تجاهله!
    })
    .then((value) => {
        console.log(value);     // 42 -- وليس 999
    });

تسلسل الوعود

تسلسل الوعود هو تقنية ربط عدة استدعاءات .then() بالتسلسل. كل .then() يُرجع وعدًا جديدًا، مما يسمح لـ .then() التالي بالانتظار حتى تكتمل العملية السابقة. هذه هي الميزة الأساسية للوعود على الاستدعاءات الراجعة -- العمليات غير المتزامنة المتسلسلة تصبح سلسلة مسطحة وقابلة للقراءة بدلاً من استدعاءات راجعة متداخلة.

مثال: سلسلة وعود أساسية

// تحويل مثال جحيم الاستدعاءات الراجعة إلى سلسلة وعود
function getUser(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: userId, name: 'Alice', teamId: 5 });
        }, 1000);
    });
}

function getTeam(teamId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: teamId, name: 'Engineering', companyId: 3 });
        }, 1000);
    });
}

function getCompany(companyId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: companyId, name: 'TechCorp' });
        }, 1000);
    });
}

// سلسلة نظيفة ومسطحة بدلاً من الاستدعاءات الراجعة المتداخلة!
getUser(1)
    .then((user) => {
        console.log('تم الحصول على المستخدم:', user.name);
        return getTeam(user.teamId);
    })
    .then((team) => {
        console.log('تم الحصول على الفريق:', team.name);
        return getCompany(team.companyId);
    })
    .then((company) => {
        console.log('تم الحصول على الشركة:', company.name);
    })
    .catch((error) => {
        console.error('فشل شيء ما:', error.message);
    });

إرجاع القيم في السلاسل

كل .then() في السلسلة يمكنه تحويل القيمة قبل تمريرها إلى الخطوة التالية. إذا أرجعت قيمة عادية، تصبح القيمة المُنجزة للوعد التالي. إذا أرجعت وعدًا، تنتظر السلسلة حلّه. إذا لم تُرجع شيئًا، يتم تمرير undefined إلى الخطوة التالية.

مثال: سلسلة تحويل البيانات

// معالجة البيانات عبر سلسلة من التحويلات
function fetchRawData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('  42, 17, 93, 5, 28, 61  ');
        }, 500);
    });
}

fetchRawData()
    .then((raw) => {
        // الخطوة 1: إزالة المسافات البيضاء
        return raw.trim();
    })
    .then((trimmed) => {
        // الخطوة 2: التقسيم إلى مصفوفة
        return trimmed.split(', ');
    })
    .then((strings) => {
        // الخطوة 3: التحويل إلى أرقام
        return strings.map(Number);
    })
    .then((numbers) => {
        // الخطوة 4: الترتيب تصاعديًا
        return numbers.sort((a, b) => a - b);
    })
    .then((sorted) => {
        // الخطوة 5: حساب الإحصائيات
        const sum = sorted.reduce((acc, n) => acc + n, 0);
        return {
            sorted: sorted,
            min: sorted[0],
            max: sorted[sorted.length - 1],
            average: sum / sorted.length
        };
    })
    .then((stats) => {
        console.log('الإحصائيات:', stats);
        // { sorted: [5, 17, 28, 42, 61, 93],
        //   min: 5, max: 93, average: 41 }
    });
مهم: خطأ شائع جدًا هو نسيان إرجاع وعد داخل معالج .then(). إذا استدعيت دالة غير متزامنة دون إرجاعها، لا تنتظر السلسلة. يعمل .then() التالي فورًا مع undefined. أرجع الوعود دائمًا في خطوات السلسلة.

مثال: خطأ الإرجاع المفقود

// خطأ: إرجاع مفقود
getUser(1)
    .then((user) => {
        getTeam(user.teamId); // <-- إرجاع مفقود!
    })
    .then((team) => {
        console.log(team); // undefined -- لم ينتظر getTeam
    });

// صحيح: مع الإرجاع
getUser(1)
    .then((user) => {
        return getTeam(user.teamId); // <-- تم الإرجاع!
    })
    .then((team) => {
        console.log(team); // { id: 5, name: "Engineering", ... }
    });

Promise.resolve() و Promise.reject()

هذه توابع ثابتة تنشئ وعودًا مُستقرة فورًا. Promise.resolve(value) تنشئ وعدًا مُنجزًا بالقيمة المعطاة. Promise.reject(reason) تنشئ وعدًا مرفوضًا بالسبب المعطى. إنها مفيدة لبدء السلاسل، وإنشاء بيانات اختبار، وتطبيع القيم التي قد تكون أو لا تكون وعودًا.

مثال: Promise.resolve() و Promise.reject()

// Promise.resolve تنشئ وعدًا مُنجزًا فورًا
const fulfilled = Promise.resolve(42);
fulfilled.then((value) => console.log(value)); // 42

// Promise.reject تنشئ وعدًا مرفوضًا فورًا
const rejected = Promise.reject(new Error('فشل'));
rejected.catch((err) => console.error(err.message)); // "فشل"

// مفيدة لبدء سلسلة
Promise.resolve()
    .then(() => fetchData())
    .then((data) => processData(data))
    .catch((error) => handleError(error));

// تطبيع القيم: تُرجع دائمًا وعدًا
function ensurePromise(valueOrPromise) {
    if (valueOrPromise instanceof Promise) {
        return valueOrPromise;
    }
    return Promise.resolve(valueOrPromise);
}

// تعمل مع القيم المتزامنة وغير المتزامنة
ensurePromise(42).then(console.log);              // 42
ensurePromise(fetchData()).then(console.log);      // [بيانات غير متزامنة]

// إذا مررت وعدًا إلى Promise.resolve، تُرجع نفس الوعد
const original = new Promise((resolve) => resolve('مرحبًا'));
const wrapped = Promise.resolve(original);
console.log(original === wrapped); // true

Promise.all() -- التنفيذ المتوازي

يأخذ Promise.all() مُكررًا (عادة مصفوفة) من الوعود ويُرجع وعدًا واحدًا يُنجز عندما تُنجز جميع وعود الإدخال. النتيجة هي مصفوفة من جميع القيم المُنجزة، بنفس ترتيب الإدخال. إذا رُفض أي واحد من وعود الإدخال، يُرفض Promise.all() فورًا بذلك الخطأ، متجاهلاً نتائج أي وعود مُنجزة.

مثال: Promise.all() للطلبات المتوازية

// محاكاة استدعاءات API بمدد مختلفة
function fetchUserProfile(id) {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ id, name: 'Alice' }), 1000);
    });
}

function fetchUserPosts(id) {
    return new Promise((resolve) => {
        setTimeout(() => resolve([
            { title: 'المنشور الأول' },
            { title: 'المنشور الثاني' }
        ]), 1500);
    });
}

function fetchUserFriends(id) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(['Bob', 'Charlie']), 800);
    });
}

// جميع الطلبات الثلاثة تعمل بالتوازي
// الوقت الإجمالي: ~1500 مللي ثانية (أبطأ طلب)، وليس 3300 مللي ثانية
Promise.all([
    fetchUserProfile(1),
    fetchUserPosts(1),
    fetchUserFriends(1)
])
.then(([profile, posts, friends]) => {
    // تفكيك مصفوفة النتائج
    console.log('الملف الشخصي:', profile);
    console.log('المنشورات:', posts);
    console.log('الأصدقاء:', friends);
})
.catch((error) => {
    console.error('فشل أحد الطلبات:', error.message);
});

مثال: Promise.all() يفشل بسرعة

// إذا رُفض أي وعد، يُرفض الكل
const promises = [
    Promise.resolve('A'),
    Promise.reject(new Error('فشل B')),
    Promise.resolve('C')  // لا يزال يعمل، لكن نتيجته تُتجاهل
];

Promise.all(promises)
    .then((results) => {
        console.log('هذا لا يعمل أبدًا');
    })
    .catch((error) => {
        console.error(error.message); // "فشل B"
    });

// نمط شائع: جلب موارد متعددة أو الفشل بالكامل
async function loadDashboard() {
    try {
        const [users, orders, analytics] = await Promise.all([
            fetch('/api/users').then(r => r.json()),
            fetch('/api/orders').then(r => r.json()),
            fetch('/api/analytics').then(r => r.json())
        ]);
        renderDashboard(users, orders, analytics);
    } catch (error) {
        showError('فشل في تحميل بيانات لوحة التحكم');
    }
}
ملاحظة: عندما يُرفض Promise.all() لأن وعدًا واحدًا فشل، لا يتم إلغاء الوعود الأخرى. تستمر في التنفيذ في الخلفية؛ نتائجها ببساطة تُتجاهل. لا يملك JavaScript آلية مدمجة لإلغاء الوعود (رغم أن واجهة AbortController يمكنها إلغاء طلبات fetch).

Promise.allSettled()

ينتظر Promise.allSettled() حتى تستقر جميع وعود الإدخال (سواء أُنجزت أو رُفضت) ويُرجع مصفوفة من الكائنات تصف نتيجة كل وعد. على عكس Promise.all()، لا يقطع أبدًا عند الرفض. هذا يجعله مثاليًا عندما تريد محاولة عمليات متعددة ومعالجة كل نتيجة بشكل مستقل، بغض النظر عما إذا فشل بعضها.

مثال: Promise.allSettled()

const promises = [
    fetch('https://api.example.com/users'),
    fetch('https://api.broken.com/data'),   // هذا سيفشل
    fetch('https://api.example.com/products')
];

Promise.allSettled(promises)
    .then((results) => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`الطلب ${index} نجح:`, result.value);
            } else {
                console.error(`الطلب ${index} فشل:`, result.reason);
            }
        });
    });

// كائنات النتيجة تبدو هكذا:
// { status: "fulfilled", value: Response }
// { status: "rejected", reason: TypeError }
// { status: "fulfilled", value: Response }

// استخدام عملي: عمليات دُفعية مع معالجة فشل جزئي
function saveMultipleRecords(records) {
    const savePromises = records.map(record =>
        fetch('/api/save', {
            method: 'POST',
            body: JSON.stringify(record)
        })
    );

    return Promise.allSettled(savePromises).then((results) => {
        const succeeded = results.filter(r => r.status === 'fulfilled');
        const failed = results.filter(r => r.status === 'rejected');

        console.log(`${succeeded.length} حُفظت، ${failed.length} فشلت`);
        return { succeeded, failed };
    });
}

Promise.race()

يُرجع Promise.race() وعدًا يستقر بمجرد استقرار أول وعد من وعود الإدخال، سواء أُنجز أو رُفض. تستمر الوعود المتبقية في العمل لكن نتائجها تُتجاهل. هذا مفيد لتنفيذ المهلات الزمنية، أو سباق مصادر بيانات متعددة، أو أخذ أول نتيجة متاحة.

مثال: Promise.race() للمهلات الزمنية

// إنشاء وعد مهلة زمنية
function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error(`انتهت مهلة العملية بعد ${ms} مللي ثانية`));
        }, ms);
    });
}

// سباق fetch ضد مهلة زمنية
function fetchWithTimeout(url, ms = 5000) {
    return Promise.race([
        fetch(url),
        timeout(ms)
    ]);
}

// الاستخدام: إذا استغرق fetch أكثر من 3 ثوانٍ، يُرفض
fetchWithTimeout('https://api.example.com/data', 3000)
    .then((response) => {
        console.log('تم الحصول على الاستجابة في الوقت!');
        return response.json();
    })
    .catch((error) => {
        console.error(error.message);
        // إما "انتهت مهلة العملية بعد 3000 مللي ثانية"
        // أو خطأ شبكة من fetch
    });

// سباق مصادر متعددة لأسرع استجابة
function fetchFromFastest(urls) {
    return Promise.race(
        urls.map(url => fetch(url).then(r => r.json()))
    );
}

fetchFromFastest([
    'https://cdn1.example.com/data.json',
    'https://cdn2.example.com/data.json',
    'https://cdn3.example.com/data.json'
]).then((data) => {
    console.log('تم الحصول على البيانات من أسرع CDN:', data);
});

Promise.any()

يُرجع Promise.any() وعدًا يُنجز بمجرد إنجاز أول وعد من وعود الإدخال. يتجاهل الرفض ما لم تُرفض جميع الوعود، وفي هذه الحالة يُرفض بـ AggregateError يحتوي على جميع أسباب الرفض. هذا عكس Promise.all() من حيث منطق الإنجاز: Promise.all() يحتاج نجاح الكل، بينما Promise.any() يحتاج نجاح واحد فقط.

مثال: Promise.any()

// محاولة مصادر متعددة، أخذ أول نجاح
const mirrors = [
    fetch('https://mirror1.example.com/package.tar.gz'),
    fetch('https://mirror2.example.com/package.tar.gz'),
    fetch('https://mirror3.example.com/package.tar.gz')
];

Promise.any(mirrors)
    .then((response) => {
        console.log('تم التنزيل من:', response.url);
    })
    .catch((aggregateError) => {
        // يصل هنا فقط إذا فشلت جميع المرايا
        console.error('فشلت جميع المرايا:');
        aggregateError.errors.forEach((err, i) => {
            console.error(`  المرآة ${i + 1}: ${err.message}`);
        });
    });

// الفرق عن Promise.race():
// race() يستقر على أول مُستقر (نجاح أو فشل)
// any() يستقر على أول مُنجز (نجاح فقط)

// إذا أرجعت أسرع مرآة خطأ، سيُرفض race()
// لكن any() سينتظر استجابة ناجحة

const example = [
    Promise.reject('خطأ 1'),   // الأسرع، لكن مرفوض
    new Promise((resolve) =>
        setTimeout(() => resolve('نجاح!'), 1000)
    )
];

Promise.race(example).catch(console.log);  // "خطأ 1" (أول مستقر)
Promise.any(example).then(console.log);    // "نجاح!" (أول مُنجز)

انتشار الأخطاء في السلاسل

أحد أهم جوانب الوعود هو كيفية انتشار الأخطاء عبر السلاسل. عندما يُرفض وعد أو يطرح معالج .then() خطأ، ينتشر الخطأ أسفل السلسلة، متخطيًا جميع معالجات .then() حتى يجد معالج .catch(). هذا مشابه لكيفية انتشار الاستثناءات عبر كتل try/catch في الكود المتزامن.

مثال: انتشار الأخطاء

Promise.resolve('بداية')
    .then((value) => {
        console.log('الخطوة 1:', value); // "الخطوة 1: بداية"
        return 'الخطوة 1 اكتملت';
    })
    .then((value) => {
        console.log('الخطوة 2:', value); // "الخطوة 2: الخطوة 1 اكتملت"
        throw new Error('فشلت الخطوة 2!');
    })
    .then((value) => {
        // يتم تخطيها -- الخطأ ينتشر
        console.log('الخطوة 3:', value);
    })
    .then((value) => {
        // يتم تخطيها -- الخطأ لا يزال ينتشر
        console.log('الخطوة 4:', value);
    })
    .catch((error) => {
        // يلتقط الخطأ من الخطوة 2
        console.error('تم الالتقاط:', error.message);
        // "تم الالتقاط: فشلت الخطوة 2!"
        return 'تم الاسترداد';
    })
    .then((value) => {
        // تستمر السلسلة بعد catch
        console.log('الخطوة 5:', value); // "الخطوة 5: تم الاسترداد"
    });

مثال: معالجات .catch() متعددة

// يمكنك وضع معالجات catch متعددة لأقسام مختلفة
fetchUserData()
    .then(validateData)
    .catch((error) => {
        // يعالج الأخطاء من fetchUserData أو validateData
        console.error('فشل التحقق من البيانات:', error.message);
        return getDefaultData(); // محاولة الرجوع
    })
    .then(transformData)
    .then(saveToDatabase)
    .catch((error) => {
        // يعالج الأخطاء من transformData أو saveToDatabase
        // يلتقط أيضًا إذا فشل getDefaultData()
        console.error('فشلت المعالجة:', error.message);
    });

// إعادة طرح الأخطاء
Promise.resolve()
    .then(() => {
        throw new Error('خطأ أصلي');
    })
    .catch((error) => {
        console.log('الالتقاط الأول:', error.message);

        if (error.message.includes('أصلي')) {
            throw error; // إعادة الطرح للانتشار أكثر
        }
        return 'تمت المعالجة';
    })
    .catch((error) => {
        console.log('الالتقاط الثاني:', error.message);
        // "الالتقاط الثاني: خطأ أصلي"
    });
مهم: رفض الوعد غير المعالج (وعد مرفوض بدون .catch()) سيسبب حدث unhandledrejection في المتصفح أو تحذيرًا (وفي النهاية انهيارًا) في Node.js. أنهِ دائمًا سلاسل الوعود بمعالج .catch()، أو استخدم معالجًا عامًا كشبكة أمان: window.addEventListener('unhandledrejection', (event) => { ... }).

أمثلة واقعية

دعونا نطبق كل ما تعلمناه لحل مشاكل عملية ستواجهها في تطوير JavaScript الواقعي.

استدعاءات API متسلسلة

عندما يعتمد كل طلب على بيانات من الطلب السابق، يجب تسلسلها بالترتيب. هذا هو النمط الذي رأيته سابقًا، لكن هنا نسخة واقعية أكثر اكتمالاً مع معالجة أخطاء مناسبة وتراكم بيانات عبر الخطوات.

مثال: استدعاءات API متسلسلة مع تراكم البيانات

function buildUserDashboard(userId) {
    let dashboardData = {};

    return fetchUser(userId)
        .then((user) => {
            dashboardData.user = user;
            return fetchPermissions(user.roleId);
        })
        .then((permissions) => {
            dashboardData.permissions = permissions;

            // جلب بيانات المشرف فقط إذا كان لدى المستخدم صلاحيات مشرف
            if (permissions.includes('admin')) {
                return fetchAdminStats();
            }
            return null;
        })
        .then((adminStats) => {
            if (adminStats) {
                dashboardData.adminStats = adminStats;
            }
            return fetchNotifications(userId);
        })
        .then((notifications) => {
            dashboardData.notifications = notifications;
            return dashboardData;
        })
        .catch((error) => {
            console.error('فشل بناء لوحة التحكم:', error.message);
            return {
                error: error.message,
                partial: dashboardData
            };
        });
}

// الاستخدام
buildUserDashboard(123).then((dashboard) => {
    if (dashboard.error) {
        renderErrorState(dashboard.error, dashboard.partial);
    } else {
        renderDashboard(dashboard);
    }
});

التحميل المتوازي للبيانات

عندما تكون الطلبات مستقلة عن بعضها البعض، تحميلها بالتوازي مع Promise.all() يقلل بشكل كبير من وقت التحميل الإجمالي. إليك نمط كامل لتحميل صفحة معقدة ببيانات مطلوبة واختيارية.

مثال: تحميل متوازي مع بيانات مطلوبة واختيارية

function loadPage(pageId) {
    // بيانات مطلوبة -- لا يمكن عرض الصفحة بدونها
    const requiredData = Promise.all([
        fetch(`/api/pages/${pageId}`).then(r => r.json()),
        fetch(`/api/pages/${pageId}/content`).then(r => r.json())
    ]);

    // بيانات اختيارية -- جيد وجودها، لكن الصفحة تعمل بدونها
    const optionalData = Promise.allSettled([
        fetch(`/api/pages/${pageId}/comments`).then(r => r.json()),
        fetch(`/api/pages/${pageId}/related`).then(r => r.json()),
        fetch('https://api.analytics.com/track').then(r => r.json())
    ]);

    // انتظار اكتمال كلا المجموعتين
    return Promise.all([requiredData, optionalData])
        .then(([[page, content], optionalResults]) => {
            const result = { page, content };

            // استخراج البيانات الاختيارية، مع قيم بديلة للفشل
            const [comments, related, analytics] = optionalResults;
            result.comments = comments.status === 'fulfilled'
                ? comments.value : [];
            result.related = related.status === 'fulfilled'
                ? related.value : [];

            return result;
        });
}

// الاستخدام
loadPage('about-us')
    .then(({ page, content, comments, related }) => {
        renderPage(page);
        renderContent(content);
        renderComments(comments); // قد تكون مصفوفة فارغة عند الفشل
        renderRelated(related);   // قد تكون مصفوفة فارغة عند الفشل
    })
    .catch((error) => {
        // يُفعّل فقط إذا فشلت البيانات المطلوبة
        showErrorPage(error.message);
    });

مثال: نمط إعادة المحاولة مع الوعود

function fetchWithRetry(url, maxRetries = 3, delay = 1000) {
    return new Promise((resolve, reject) => {
        function attempt(retriesLeft) {
            fetch(url)
                .then((response) => {
                    if (!response.ok) {
                        throw new Error(`HTTP ${response.status}`);
                    }
                    return response.json();
                })
                .then(resolve)
                .catch((error) => {
                    if (retriesLeft <= 0) {
                        reject(new Error(
                            `فشل بعد ${maxRetries} محاولات: ${error.message}`
                        ));
                        return;
                    }

                    console.log(
                        `إعادة المحاولة خلال ${delay} مللي ثانية. المحاولات المتبقية: ${retriesLeft}`
                    );

                    setTimeout(() => {
                        attempt(retriesLeft - 1);
                    }, delay);
                });
        }

        attempt(maxRetries);
    });
}

// الاستخدام
fetchWithRetry('/api/flaky-endpoint', 3, 2000)
    .then((data) => console.log('تم الحصول على البيانات:', data))
    .catch((error) => console.error('تم التخلي:', error.message));

مثال: معالجة دُفعية متسلسلة

// معالجة العناصر واحدًا تلو الآخر لتجنب إثقال الخادم
function processSequentially(items, processFn) {
    return items.reduce((chain, item) => {
        return chain.then((results) => {
            return processFn(item).then((result) => {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}

// الاستخدام: رفع الملفات واحدًا تلو الآخر
const files = ['report.pdf', 'photo.jpg', 'data.csv'];

function uploadFile(filename) {
    return new Promise((resolve) => {
        console.log(`جاري رفع ${filename}...`);
        setTimeout(() => {
            console.log(`تم رفع ${filename}!`);
            resolve({ filename, status: 'uploaded' });
        }, 1000);
    });
}

processSequentially(files, uploadFile)
    .then((results) => {
        console.log('اكتمل رفع الكل:', results);
        // [
        //   { filename: "report.pdf", status: "uploaded" },
        //   { filename: "photo.jpg", status: "uploaded" },
        //   { filename: "data.csv", status: "uploaded" }
        // ]
    });

أنماط مضادة يجب تجنبها مع الوعود

معرفة ما لا يجب فعله لا يقل أهمية عن معرفة الأنماط الصحيحة. إليك أكثر الأخطاء شيوعًا التي يرتكبها المطورون عند العمل مع الوعود.

مثال: الأنماط المضادة الشائعة

// النمط المضاد 1: "النمط المضاد لمُنشئ الوعد"
// تغليف وعد موجود في وعد جديد غير ضروري
// سيء
function fetchDataBad(url) {
    return new Promise((resolve, reject) => {
        fetch(url).then(resolve).catch(reject);
    });
}
// جيد -- فقط أرجع الوعد مباشرة
function fetchDataGood(url) {
    return fetch(url);
}

// النمط المضاد 2: وعود متداخلة (جحيم الوعود)
// سيء -- هذا مجرد جحيم استدعاءات راجعة مع وعود
function getUserDataBad(id) {
    return getUser(id).then((user) => {
        return getPosts(user.id).then((posts) => {
            return getComments(posts[0].id).then((comments) => {
                return { user, posts, comments };
            });
        });
    });
}
// جيد -- سلسلة مسطحة مع بيانات متراكمة
function getUserDataGood(id) {
    let userData = {};
    return getUser(id)
        .then((user) => {
            userData.user = user;
            return getPosts(user.id);
        })
        .then((posts) => {
            userData.posts = posts;
            return getComments(posts[0].id);
        })
        .then((comments) => {
            userData.comments = comments;
            return userData;
        });
}

// النمط المضاد 3: الإرجاع المنسي
// سيء -- لا إرجاع يعني أن السلسلة لا تنتظر
somePromise.then((value) => {
    anotherAsyncOperation(value); // <-- لا إرجاع!
}).then((result) => {
    // result هو undefined
});

// النمط المضاد 4: استخدام .then() للتحويلات المتزامنة
// ليس خطأ، لكن مطول بشكل مفرط للعمليات المتزامنة
Promise.resolve(5)
    .then(x => x * 2)
    .then(x => x + 1)
    .then(x => String(x));
// فكر في async/await للقراءة في هذه الحالات
نصيحة احترافية: الوعود هي الأساس لـ async/await، الذي ستتعلمه في الدرس التالي. كل دالة async تُرجع وعدًا، و await هو سكر صياغي لـ .then(). فهم الوعود بعمق يجعل async/await بديهيًا ويساعدك في تصحيح المشكلات التي تنشأ عند خلط الصياغتين.

تمرين عملي

ابنِ خط معالجة بيانات كاملًا باستخدام الوعود. أنشئ الدوال التالية التي تحاكي استدعاءات API باستخدام setTimeout داخل new Promise(): (1) fetchUsers() التي تُرجع مصفوفة من 5 كائنات مستخدم بعد 500 مللي ثانية. (2) fetchUserDetails(userId) التي تُرجع معلومات مستخدم مفصلة بعد 300 مللي ثانية، وتُرفض إذا كان userId هو 3 (لمحاكاة فشل). (3) fetchUserActivity(userId) التي تُرجع سجل نشاط بعد 400 مللي ثانية. ثم نفّذ هذه الأنماط: (أ) استخدم Promise.all() لجلب تفاصيل جميع المستخدمين الـ 5 بالتوازي، وتعامل مع حالة فشل userId 3. (ب) استخدم Promise.allSettled() لجلب تفاصيل جميع المستخدمين وعرض أيها نجح وأيها فشل. (ج) أنشئ دالة fetchWithTimeout(promise, ms) باستخدام Promise.race() التي تُرفض إذا استغرق الوعد المعطى وقتًا أطول من الوقت المحدد. (د) سلسل fetchUsers() ثم fetchUserDetails() ثم fetchUserActivity() بالتسلسل، مع تراكم البيانات في كل خطوة، مع .catch() يوفر نتيجة جزئية. (هـ) نفّذ دالة retryPromise(fn, maxRetries) التي تعيد محاولة دالة تُرجع وعدًا فاشلاً حتى العدد المحدد من المرات. اختبر كل نمط وسجّل النتائج في وحدة التحكم.