أساسيات JavaScript

Async/Await - البرمجة غير المتزامنة

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

مقدمة في Async/Await

Async/Await هو صيغة حديثة في JavaScript تم تقديمها في ES2017 (ES8) تجعل الكود غير المتزامن يبدو ويتصرف مثل الكود المتزامن. قبل async/await، كان المطورون يعتمدون على دوال الاستدعاء الراجع (callbacks) وسلاسل Promise مع .then() و.catch() للتعامل مع العمليات غير المتزامنة. بينما كانت Promises تحسينا كبيرا مقارنة بجحيم الاستدعاءات الراجعة، الا ان سلاسل .then() المتداخلة بعمق كانت لا تزال صعبة القراءة والصيانة. Async/await يحل هذه المشكلة بتوفير طريقة انظف وابسط للعمل مع Promises.

في جوهره، async/await هو سكر نحوي مبني فوق Promises. هو لا يستبدل Promises -- بل يجعل استخدامها اسهل. دالة async تعيد دائما Promise، والكلمة المفتاحية await توقف تنفيذ الدالة حتى يتم حل او رفض الـ Promise المنتظر. هذا يسمح لك بكتابة كود غير متزامن يقرا من الاعلى الى الاسفل تماما مثل الكود المتزامن، مما يجعله اسهل بشكل كبير في الفهم والتصحيح والصيانة.

اعلان دالة Async

لانشاء دالة async، ما عليك سوى وضع الكلمة المفتاحية async قبل تعريف function. هذا يخبر JavaScript ان الدالة ستحتوي على عمليات غير متزامنة وانها يجب ان تعيد دائما Promise. اذا اعادت الدالة قيمة مباشرة، يقوم JavaScript تلقائيا بتغليف تلك القيمة في Promise محلول.

مثال: اعلان دالة Async الاساسي

// اعلان دالة async
async function greetUser() {
    return 'مرحبا بالعالم!';
}

// الدالة تعيد Promise
greetUser().then(message => {
    console.log(message); // المخرج: مرحبا بالعالم!
});

// يمكنك ايضا التحقق من انها تعيد Promise
console.log(greetUser() instanceof Promise); // المخرج: true

على الرغم من ان الدالة اعلاه تبدو وكانها تعيد نصا بسيطا، الا ان JavaScript يغلف القيمة المعادة في Promise.resolve('مرحبا بالعالم!') خلف الكواليس. لهذا السبب يمكنك ربط .then() على النتيجة. اذا القيت خطا داخل دالة async، فانها تعيد تلقائيا Promise مرفوضا.

مثال: دالة Async تعيد Promise مرفوضا

async function failingFunction() {
    throw new Error('حدث خطا ما!');
}

failingFunction().catch(error => {
    console.log(error.message); // المخرج: حدث خطا ما!
});

الكلمة المفتاحية Await

الكلمة المفتاحية await يمكن استخدامها فقط داخل دالة async (مع استثناء واحد سنتناوله لاحقا). عندما تضع await قبل Promise، يوقف JavaScript تنفيذ الدالة غير المتزامنة عند تلك النقطة وينتظر حتى يستقر الـ Promise. اذا تم حل الـ Promise، يعيد await القيمة المحلولة. اذا تم رفض الـ Promise، يرمي await سبب الرفض كخطا يمكنك التقاطه باستخدام try/catch.

مثال: استخدام Await مع Promise

function fetchUserData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: 1, name: 'ادريس', role: 'مطور' });
        }, 2000);
    });
}

async function displayUser() {
    console.log('جاري جلب بيانات المستخدم...');
    const user = await fetchUserData(); // يتوقف هنا لمدة ثانيتين
    console.log('المستخدم:', user.name);   // المخرج بعد ثانيتين: المستخدم: ادريس
    console.log('الدور:', user.role);       // المخرج: الدور: مطور
}

displayUser();
ملاحظة: الكلمة المفتاحية await توقف فقط تنفيذ الدالة غير المتزامنة الموجودة بداخلها. باقي برنامجك يستمر في العمل بشكل طبيعي. معالجات الاحداث الاخرى والمؤقتات والدوال لا يتم حظرها اثناء انتظار الدالة غير المتزامنة.

مقارنة Async/Await مع سلاسل .then()

لتقدير async/await حقا، دعنا نقارن نفس العملية غير المتزامنة مكتوبة بسلاسل .then() مقابل async/await. فكر في سيناريو تحتاج فيه الى جلب مستخدم، ثم جلب طلباته، واخيرا جلب تفاصيل احدث طلب.

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

function getUser(userId) {
    return fetch('/api/users/' + userId).then(res => res.json());
}
function getOrders(userId) {
    return fetch('/api/users/' + userId + '/orders').then(res => res.json());
}
function getOrderDetails(orderId) {
    return fetch('/api/orders/' + orderId).then(res => res.json());
}

// استخدام سلاسل .then() -- يصبح متداخلا بعمق
getUser(1)
    .then(user => {
        console.log('المستخدم:', user.name);
        return getOrders(user.id);
    })
    .then(orders => {
        const latestOrder = orders[0];
        console.log('احدث طلب:', latestOrder.id);
        return getOrderDetails(latestOrder.id);
    })
    .then(details => {
        console.log('تفاصيل الطلب:', details);
    })
    .catch(error => {
        console.error('خطا:', error.message);
    });

مثال: استخدام Async/Await -- اكثر وضوحا

async function displayLatestOrder() {
    try {
        const user = await getUser(1);
        console.log('المستخدم:', user.name);

        const orders = await getOrders(user.id);
        const latestOrder = orders[0];
        console.log('احدث طلب:', latestOrder.id);

        const details = await getOrderDetails(latestOrder.id);
        console.log('تفاصيل الطلب:', details);
    } catch (error) {
        console.error('خطا:', error.message);
    }
}

displayLatestOrder();

نسخة async/await تقرا مثل اجراء بسيط خطوة بخطوة. كل سطر يوضح بشكل واضح ما يحدث بعد ذلك. لا يوجد تداخل ولا مسافة بادئة للاستدعاءات الراجعة ولا ارتباك حول تدفق البيانات. معالجة الاخطاء ايضا مركزة في كتلة try/catch واحدة بدلا من .catch() واحد في نهاية السلسلة.

معالجة الاخطاء باستخدام Try/Catch

واحدة من اقوى ميزات async/await هي انه يمكنك استخدام كتل try/catch القياسية لمعالجة الاخطاء، تماما كما تفعل في الكود المتزامن. هذا يستبدل طريقة .catch() المستخدمة مع سلاسل Promise ويمنحك تحكما دقيقا في كيفية معالجة الاخطاء المختلفة.

مثال: معالجة شاملة للاخطاء

async function fetchUserProfile(userId) {
    try {
        const response = await fetch('/api/users/' + userId);

        // التحقق مما اذا كانت استجابة HTTP ناجحة
        if (!response.ok) {
            throw new Error('خطا HTTP! الحالة: ' + response.status);
        }

        const user = await response.json();
        console.log('تم تحميل الملف الشخصي:', user.name);
        return user;

    } catch (error) {
        if (error.name === 'TypeError') {
            // خطا في الشبكة (لا اتصال، فشل DNS، الخ.)
            console.error('خطا في الشبكة: تعذر الوصول الى الخادم.');
        } else {
            // خطا HTTP او اخطاء اخرى
            console.error('فشل تحميل الملف الشخصي:', error.message);
        }
        return null; // اعادة قيمة افتراضية عند الفشل
    }
}

// استخدام الدالة
async function main() {
    const profile = await fetchUserProfile(42);
    if (profile) {
        console.log('مرحبا بعودتك، ' + profile.name);
    } else {
        console.log('يرجى المحاولة لاحقا.');
    }
}

main();

يمكنك ايضا استخدام كتلة finally لتشغيل كود التنظيف بغض النظر عما اذا نجحت العملية او فشلت. هذا مفيد لاخفاء مؤشرات التحميل واغلاق الاتصالات او اعادة تعيين الحالة.

مثال: نمط Try/Catch/Finally

async function loadDashboardData() {
    const loadingSpinner = document.getElementById('spinner');
    loadingSpinner.style.display = 'block';

    try {
        const stats = await fetch('/api/dashboard/stats').then(r => r.json());
        const notifications = await fetch('/api/notifications').then(r => r.json());
        renderDashboard(stats, notifications);
    } catch (error) {
        showErrorMessage('فشل تحميل بيانات لوحة المعلومات.');
        console.error(error);
    } finally {
        // هذا يعمل سواء نجحت كتلة try او فشلت
        loadingSpinner.style.display = 'none';
    }
}
نصيحة احترافية: يمكنك تداخل كتل try/catch داخل دالة async لمعالجة الاخطاء المختلفة بشكل منفصل. على سبيل المثال، قد ترغب في متابعة تحميل لوحة المعلومات حتى لو فشل تحميل الاشعارات. غلف كل عملية مستقلة في try/catch خاص بها لمنع فشل واحد من ايقاف كل شيء.

عمليات Await متعددة: التنفيذ المتسلسل مقابل المتوازي

خطا شائع مع async/await هو جعل العمليات متسلسلة عن طريق الخطا عندما يمكن ان تعمل بالتوازي. عندما تضع عدة عبارات await واحدة تلو الاخرى، كل واحدة تنتظر السابقة لتكتمل قبل البدء. اذا كانت تلك العمليات مستقلة عن بعضها البعض، فانت تهدر الوقت.

مثال: التنفيذ المتسلسل (ابطا)

async function loadPageData() {
    console.time('متسلسل');

    // هذه الطلبات الثلاثة تعمل واحدا تلو الاخر
    const users = await fetch('/api/users').then(r => r.json());       // ثانية واحدة
    const posts = await fetch('/api/posts').then(r => r.json());       // ثانية واحدة
    const comments = await fetch('/api/comments').then(r => r.json()); // ثانية واحدة

    console.timeEnd('متسلسل'); // المجموع: ~3 ثوان
    return { users, posts, comments };
}

مثال: التنفيذ المتوازي باستخدام Promise.all (اسرع)

async function loadPageData() {
    console.time('متوازي');

    // جميع الطلبات الثلاثة تبدا في نفس الوقت
    const [users, posts, comments] = await Promise.all([
        fetch('/api/users').then(r => r.json()),
        fetch('/api/posts').then(r => r.json()),
        fetch('/api/comments').then(r => r.json())
    ]);

    console.timeEnd('متوازي'); // المجموع: ~ثانية واحدة (اطول طلب)
    return { users, posts, comments };
}

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

مثال: Promise.allSettled للمعالجة المرنة

async function loadPageDataSafely() {
    const results = await Promise.allSettled([
        fetch('/api/users').then(r => r.json()),
        fetch('/api/posts').then(r => r.json()),
        fetch('/api/comments').then(r => r.json())
    ]);

    // كل نتيجة لها حالة: 'fulfilled' او 'rejected'
    const users = results[0].status === 'fulfilled' ? results[0].value : [];
    const posts = results[1].status === 'fulfilled' ? results[1].value : [];
    const comments = results[2].status === 'fulfilled' ? results[2].value : [];

    return { users, posts, comments };
}
خطا شائع: استخدام Promise.all() عندما لا يجب ان يمنع فشل طلب واحد الطلبات الاخرى. اذا كنت تجلب ملف المستخدم والاشعارات والتوصيات، فشل التوصيات لا يجب ان يمنع تحميل الملف الشخصي. استخدم Promise.allSettled() بدلا من ذلك، الذي ينتظر اكتمال جميع Promises بغض النظر عما اذا تم حلها او رفضها.

Await على المستوى الاعلى

في JavaScript الحديث (ES2022)، يمكنك استخدام await على المستوى الاعلى في وحدات ES بدون تغليفها في دالة async. هذا يسمى top-level await. وهو مدعوم في المتصفحات الحديثة و Node.js (الاصدار 14.8+ مع وحدات ES). يعتبر مفيدا بشكل خاص لتهيئة الوحدات التي تعتمد على عمليات غير متزامنة.

مثال: Await على المستوى الاعلى في وحدات ES

// config.mjs -- وحدة ES
const response = await fetch('/api/config');
const config = await response.json();

export default config;

// app.mjs -- استيراد الوحدة
import config from './config.mjs';
// config محمل وجاهز بالفعل
console.log('التطبيق يعمل على المنفذ:', config.port);
ملاحظة: await على المستوى الاعلى يعمل فقط في وحدات ES (الملفات ذات الامتداد .mjs او type="module" في وسم <script> او package.json). لا يعمل في السكربتات العادية او وحدات CommonJS. عندما تستخدم وحدة await على المستوى الاعلى، فان اي وحدة تستوردها ستنتظر اكتمال العملية غير المتزامنة قبل التنفيذ.

دوال الاسهم غير المتزامنة

يمكنك دمج الكلمة المفتاحية async مع صيغة دوال الاسهم لاعلانات دوال اكثر اختصارا. دوال الاسهم غير المتزامنة مفيدة بشكل خاص للاستدعاءات الراجعة ومعالجات الاحداث والدوال المساعدة القصيرة.

مثال: صيغة دالة الاسهم غير المتزامنة

// دالة اسهم async عادية
const fetchUser = async (id) => {
    const response = await fetch('/api/users/' + id);
    return response.json();
};

// دالة اسهم async مع معامل واحد (لا حاجة لاقواس)
const getPost = async id => {
    const response = await fetch('/api/posts/' + id);
    return response.json();
};

// استخدام دالة اسهم async كاستدعاء راجع
const userIds = [1, 2, 3, 4, 5];

// ملاحظة: هذا لا يعمل كما هو متوقع مع forEach!
// forEach لا تنتظر الاستدعاءات الراجعة غير المتزامنة
userIds.forEach(async (id) => {
    const user = await fetchUser(id);
    console.log(user.name); // الترتيب غير متوقع
});

// استخدم حلقة for...of بدلا من ذلك للتنفيذ المتسلسل
async function loadAllUsers() {
    for (const id of userIds) {
        const user = await fetchUser(id);
        console.log(user.name); // ترتيب متسلسل مضمون
    }
}

// او استخدم Promise.all للتنفيذ المتوازي
async function loadAllUsersParallel() {
    const users = await Promise.all(
        userIds.map(id => fetchUser(id))
    );
    users.forEach(user => console.log(user.name));
}

الدوال غير المتزامنة في الفئات

الدوال غير المتزامنة تعمل بسلاسة كدوال داخل فئات JavaScript. هذا شائع عند بناء الخدمات والمستودعات والمتحكمات في JavaScript الكائنية التوجه. يمكنك ايضا استخدام الدوال غير المتزامنة في كائنات حرفية.

مثال: دوال غير متزامنة في فئة

class UserService {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }

    async getUser(id) {
        const response = await fetch(this.baseUrl + '/users/' + id);
        if (!response.ok) {
            throw new Error('المستخدم غير موجود');
        }
        return response.json();
    }

    async createUser(userData) {
        const response = await fetch(this.baseUrl + '/users', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(userData)
        });
        return response.json();
    }

    async updateUser(id, updates) {
        const response = await fetch(this.baseUrl + '/users/' + id, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(updates)
        });
        return response.json();
    }

    async deleteUser(id) {
        const response = await fetch(this.baseUrl + '/users/' + id, {
            method: 'DELETE'
        });
        return response.ok;
    }
}

// استخدام الفئة
const userService = new UserService('https://api.example.com');

async function manageUsers() {
    const newUser = await userService.createUser({
        name: 'احمد',
        email: 'ahmad@example.com'
    });
    console.log('تم انشاء المستخدم:', newUser.id);

    const user = await userService.getUser(newUser.id);
    console.log('تم جلب المستخدم:', user.name);
}

نمط Async IIFE

قبل ان يكون top-level await متاحا، استخدم المطورون تعبيرات الدوال المستدعاة فوريا (IIFE) مع async لتشغيل await على المستوى الاعلى من سكربتاتهم. هذا النمط لا يزال مفيدا في البيئات التي لا تدعم top-level await او عندما تريد تغليف منطق التهيئة غير المتزامن.

مثال: نمط Async IIFE

// Async IIFE -- يعمل فورا
(async () => {
    try {
        const response = await fetch('/api/config');
        const config = await response.json();
        console.log('تم تحميل اعدادات التطبيق:', config);

        // تهيئة التطبيق
        initializeApp(config);
    } catch (error) {
        console.error('فشل بدء التطبيق:', error.message);
    }
})();

// Async IIFE مسماة لتتبع افضل للاخطاء
(async function bootstrap() {
    const db = await connectToDatabase();
    const server = await startServer(db);
    console.log('الخادم يعمل على المنفذ', server.port);
})();

الاخطاء الشائعة مع Async/Await

حتى المطورين ذوي الخبرة يرتكبون اخطاء مع async/await. اليك المزالق الاكثر شيوعا وكيفية تجنبها.

الخطا 1: نسيان الكلمة المفتاحية Await

اذا استدعيت دالة async بدون await، تحصل على كائن Promise بدلا من القيمة المحلولة. هذا يمكن ان يؤدي الى اخطاء يصعب تتبعها لان الكود لا يرمي خطا -- انما ينتج نتائج غير متوقعة.

مثال: نسيان Await

async function getUserName(id) {
    const response = await fetch('/api/users/' + id);
    const user = await response.json();
    return user.name;
}

async function displayGreeting() {
    // خطا: await مفقود! name هو Promise وليس نصا
    const name = getUserName(1);
    console.log('مرحبا، ' + name); // المخرج: مرحبا، [object Promise]

    // صحيح: استخدم await للحصول على القيمة الفعلية
    const correctName = await getUserName(1);
    console.log('مرحبا، ' + correctName); // المخرج: مرحبا، ادريس
}

الخطا 2: استخدام Async غير الضروري

تعليم دالة بـ async عندما لا تستخدم await هو غير ضروري. يضيف عبئا اضافيا (تغليف القيمة المعادة في Promise) بدون اي فائدة. اذا كنت تعيد ببساطة Promise اخر، فلست بحاجة الى async.

مثال: Async غير ضروري

// غير ضروري: هذا async لا يفعل شيئا مفيدا
async function getUser(id) {
    return fetch('/api/users/' + id).then(r => r.json());
}

// افضل: اعد Promise مباشرة
function getUser(id) {
    return fetch('/api/users/' + id).then(r => r.json());
}

// مناسب: استخدام async لاننا نحتاج await
async function getUser(id) {
    const response = await fetch('/api/users/' + id);
    if (!response.ok) throw new Error('المستخدم غير موجود');
    return response.json();
}

الخطا 3: عدم معالجة الاخطاء

اذا لم تلتقط الاخطاء من Promise منتظر، ستعيد الدالة غير المتزامنة Promise مرفوضا. اذا لم يلتقط احد ذلك الرفض ايضا، ستحصل على تحذير برفض promise غير معالج.

مثال: اخطاء غير معالجة

// سيء: لا معالجة للاخطاء
async function riskyOperation() {
    const data = await fetch('/api/might-fail'); // قد يرمي خطا!
    return data.json();
}

// هذا سيسبب رفض promise غير معالج اذا فشل fetch
riskyOperation();

// جيد: معالجة صحيحة للاخطاء
async function safeOperation() {
    try {
        const data = await fetch('/api/might-fail');
        return data.json();
    } catch (error) {
        console.error('فشلت العملية:', error.message);
        return null;
    }
}

// او معالجة الاخطاء في موقع الاستدعاء
riskyOperation().catch(error => {
    console.error('تم الالتقاط في موقع الاستدعاء:', error.message);
});

الخطا 4: Await داخل الحلقات عندما يكون التنفيذ المتوازي افضل

مثال: حلقة غير فعالة مقابل التنفيذ المتوازي

const productIds = [101, 102, 103, 104, 105];

// بطيء: كل طلب ينتظر السابق
async function loadProductsSequential() {
    const products = [];
    for (const id of productIds) {
        const product = await fetch('/api/products/' + id).then(r => r.json());
        products.push(product);
    }
    return products; // ياخذ 5 اضعاف الوقت اللازم
}

// سريع: جميع الطلبات تبدا في وقت واحد
async function loadProductsParallel() {
    const products = await Promise.all(
        productIds.map(id =>
            fetch('/api/products/' + id).then(r => r.json())
        )
    );
    return products; // ياخذ فقط مدة اطول طلب
}
خطا شائع: استخدام await داخل Array.forEach(). دالة forEach لا تفهم Promises ولن تنتظر اكتمال الاستدعاء الراجع غير المتزامن. استخدم حلقة for...of للتنفيذ المتسلسل او Promise.all() مع map() للتنفيذ المتوازي.

انماط العالم الحقيقي

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

النمط 1: جلب البيانات مع حالات التحميل والخطا

مثال: نمط جلب بيانات كامل

class DataLoader {
    constructor() {
        this.loading = false;
        this.error = null;
        this.data = null;
    }

    async load(url) {
        this.loading = true;
        this.error = null;

        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error('الخادم اعاد ' + response.status);
            }
            this.data = await response.json();
            return this.data;
        } catch (error) {
            this.error = error.message;
            this.data = null;
            throw error;
        } finally {
            this.loading = false;
        }
    }
}

// الاستخدام
const loader = new DataLoader();

async function renderPage() {
    try {
        const articles = await loader.load('/api/articles');
        document.getElementById('content').innerHTML =
            articles.map(a => '<h3>' + a.title + '</h3>').join('');
    } catch (error) {
        document.getElementById('content').innerHTML =
            '<p class="error">فشل تحميل المقالات: ' + loader.error + '</p>';
    }
}

النمط 2: منطق اعادة المحاولة للشبكات غير الموثوقة

مثال: Fetch مع اعادة محاولة تلقائية

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    let lastError;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) {
                throw new Error('HTTP ' + response.status);
            }
            return await response.json();
        } catch (error) {
            lastError = error;
            console.warn('المحاولة ' + attempt + ' فشلت: ' + error.message);

            if (attempt < maxRetries) {
                // الانتظار قبل اعادة المحاولة: 1ث، 2ث، 4ث (تراجع اسي)
                const delay = Math.pow(2, attempt - 1) * 1000;
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }

    throw new Error('فشل بعد ' + maxRetries + ' محاولات: ' + lastError.message);
}

// الاستخدام
async function loadData() {
    try {
        const data = await fetchWithRetry('/api/important-data');
        console.log('تم تحميل البيانات بنجاح:', data);
    } catch (error) {
        console.error('فشلت جميع المحاولات:', error.message);
    }
}

النمط 3: تدفق مصادقة المستخدم

مثال: تدفق مصادقة كامل

class AuthService {
    constructor(apiBase) {
        this.apiBase = apiBase;
        this.token = null;
        this.user = null;
    }

    async login(email, password) {
        try {
            const response = await fetch(this.apiBase + '/auth/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ email, password })
            });

            if (!response.ok) {
                const errorData = await response.json();
                throw new Error(errorData.message || 'فشل تسجيل الدخول');
            }

            const data = await response.json();
            this.token = data.token;
            localStorage.setItem('authToken', this.token);

            // جلب ملف المستخدم بعد تسجيل الدخول الناجح
            this.user = await this.getProfile();
            return this.user;

        } catch (error) {
            this.token = null;
            this.user = null;
            throw error;
        }
    }

    async getProfile() {
        if (!this.token) {
            throw new Error('غير مصادق');
        }

        const response = await fetch(this.apiBase + '/auth/profile', {
            headers: {
                'Authorization': 'Bearer ' + this.token,
                'Accept': 'application/json'
            }
        });

        if (!response.ok) {
            if (response.status === 401) {
                this.logout();
                throw new Error('انتهت الجلسة. يرجى تسجيل الدخول مرة اخرى.');
            }
            throw new Error('فشل تحميل الملف الشخصي');
        }

        return response.json();
    }

    async logout() {
        try {
            if (this.token) {
                await fetch(this.apiBase + '/auth/logout', {
                    method: 'POST',
                    headers: { 'Authorization': 'Bearer ' + this.token }
                });
            }
        } finally {
            this.token = null;
            this.user = null;
            localStorage.removeItem('authToken');
        }
    }

    async refreshToken() {
        const response = await fetch(this.apiBase + '/auth/refresh', {
            method: 'POST',
            headers: { 'Authorization': 'Bearer ' + this.token }
        });

        if (!response.ok) {
            throw new Error('فشل تحديث الرمز');
        }

        const data = await response.json();
        this.token = data.token;
        localStorage.setItem('authToken', this.token);
        return this.token;
    }
}

// الاستخدام
const auth = new AuthService('https://api.example.com');

async function handleLogin(formData) {
    const submitButton = document.getElementById('loginBtn');
    const errorDiv = document.getElementById('loginError');
    submitButton.disabled = true;
    errorDiv.textContent = '';

    try {
        const user = await auth.login(formData.email, formData.password);
        console.log('مرحبا، ' + user.name);
        window.location.href = '/dashboard';
    } catch (error) {
        errorDiv.textContent = error.message;
    } finally {
        submitButton.disabled = false;
    }
}

النمط 4: معالجة الملفات بالتسلسل

مثال: معالجة الملفات واحدا تلو الاخر

async function processFiles(files) {
    const results = [];

    for (const file of files) {
        try {
            console.log('جاري المعالجة:', file.name);
            const content = await readFileAsync(file);
            const processed = await uploadFile(file.name, content);
            results.push({ name: file.name, status: 'نجاح', url: processed.url });
        } catch (error) {
            results.push({ name: file.name, status: 'فشل', error: error.message });
        }
    }

    return results;
}

function readFileAsync(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(new Error('فشل قراءة ' + file.name));
        reader.readAsArrayBuffer(file);
    });
}

async function uploadFile(name, content) {
    const formData = new FormData();
    formData.append('file', new Blob([content]), name);

    const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
    });
    return response.json();
}

دمج Async/Await مع ادوات Promise المساعدة

يعمل async/await بشكل رائع مع طرق Promise المساعدة الاخرى مثل Promise.race() وPromise.any().

مثال: مهلة زمنية باستخدام Promise.race

function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => reject(new Error('انتهت مهلة الطلب بعد ' + ms + 'مللي ثانية')), ms);
    });
}

async function fetchWithTimeout(url, ms = 5000) {
    try {
        // سباق بين fetch ومهلة زمنية
        const response = await Promise.race([
            fetch(url),
            timeout(ms)
        ]);
        return await response.json();
    } catch (error) {
        console.error(error.message);
        return null;
    }
}

// الاستخدام
const data = await fetchWithTimeout('/api/slow-endpoint', 3000);

مثال: اسرع خادم مرآة باستخدام Promise.any

async function fetchFromFastestMirror(path) {
    const mirrors = [
        'https://mirror1.example.com',
        'https://mirror2.example.com',
        'https://mirror3.example.com'
    ];

    try {
        // يعيد النتيجة من اي خادم مرآة يستجيب اولا
        const response = await Promise.any(
            mirrors.map(mirror => fetch(mirror + path))
        );
        return await response.json();
    } catch (error) {
        // فشلت جميع الخوادم المرآة
        throw new Error('جميع الخوادم المرآة معطلة');
    }
}

المولدات غير المتزامنة و for-await-of

تدعم JavaScript ايضا المولدات غير المتزامنة التي تجمع بين async/await ودوال المولدات. حلقة for await...of تسمح لك بالتكرار على تدفقات البيانات غير المتزامنة.

مثال: مولد غير متزامن لـ API مقسم لصفحات

async function* fetchAllPages(baseUrl) {
    let page = 1;
    let hasMore = true;

    while (hasMore) {
        const response = await fetch(baseUrl + '?page=' + page);
        const data = await response.json();

        yield data.items;

        hasMore = data.hasNextPage;
        page++;
    }
}

// استخدام for-await-of لاستهلاك المولد غير المتزامن
async function loadAllItems() {
    const allItems = [];

    for await (const items of fetchAllPages('/api/products')) {
        allItems.push(...items);
        console.log('تم تحميل ' + allItems.length + ' عنصر حتى الان...');
    }

    console.log('اجمالي العناصر المحملة:', allItems.length);
    return allItems;
}
نصيحة احترافية: عند الاختيار بين await المتسلسل في حلقة مقابل Promise.all()، فكر في تحديد معدل الطلبات. العديد من واجهات API تحد من عدد الطلبات التي يمكنك ارسالها في الثانية. في تلك الحالات، التنفيذ المتسلسل او التنفيذ المتوازي على دفعات (معالجة مجموعات من 5-10 في المرة) هو اكثر ملاءمة من ارسال جميع الطلبات دفعة واحدة.

تمرين عملي

قم ببناء تطبيق مدير مهام يوضح جميع انماط async/await المشمولة في هذا الدرس. انشئ فئة TaskManager مع دوال async لـ loadTasks() وaddTask(task) وupdateTask(id, updates) وdeleteTask(id). كل دالة يجب ان تستخدم try/catch لمعالجة الاخطاء وكتلة finally لتحديث حالة التحميل. اضف دالة loadDashboard() تستخدم Promise.all() لتحميل المهام وملف المستخدم والاشعارات بالتوازي. اضف دالة syncAllTasks(tasks) تعالج مصفوفة من المهام بالتسلسل باستخدام حلقة for...of (محاكاة استدعاءات API محدودة المعدل). اخيرا، انشئ دالة مساعدة fetchWithTimeout() باستخدام Promise.race() ترفض اذا استغرق الطلب اكثر من 5 ثوان. اختبر جميع الدوال وتحقق من ان الجلب المتوازي اسرع من الجلب المتسلسل بقياس وقت التنفيذ باستخدام console.time().