چهارشنبه, 26 آذر 1404

مدیریت عملیات غیرهمزمان

...

” جاوااسکریپت به صورت پیش‌فرض همزمان (Synchronous) عمل می‌کنه، اما برای کارهای زمان‌بر مثل دریافت داده از سرور، از عملیات غیرهمزمان (Asynchronous) استفاده می‌کنیم. این مقاله Callback، Promise و async/await را با مثال‌های ساده توضیح میده. “

یکی از موضوعاتی که موقع کار با جاوااسکریپت حتماً باهاش مواجه می‌شیم، نحوه مدیریت عملیات‌های غیرهمزمان (Asynchronous) هست. این عملیات‌ها کارهایی هستن که زمان‌بر هستن و نمی‌تونیم منتظر تموم شدنشون بمونیم تا بقیه کد اجرا بشن.

داستان از کجا شروع شد؟

بیاین با یه مثال روزمره شروع کنیم: فرض کنید می‌خواید غذای موردعلاقتون رو از رستوران سفارش بدید:

  1. همزمان (Synchronous): شما جلوی رستوران وایمیستید و منتظر می‌مونید تا غذا آماده بشه، تحویل بگیرید، بعد برید خونه. این‌طوری کلی وقت تلف شده!

  2. غیرهمزمان (Asynchronous): شما سفارش میدید، شماره میگیرید و میرید خرید کنید یا کارهای دیگه‌تون رو انجام بدید. وقتی غذا آماده شد، بهتون خبر میدن :)

جاوااسکریپت هم دقیقاً همین شکلی کار می‌کنه! بعضی کارها مثل:

  • گرفتن اطلاعات از سرور (API)

  • خواندن فایل

  • تایمر و زمان‌بندی‌ها

همه این‌ها زمان‌بر هستن و نمی‌تونیم کل برنامه رو متوقف کنیم تا تموم بشن.

۱. Callback: راه‌حل اولیه

اولین روشی که برای حل این مشکل ابداع شد، Callback بود. کلمه Callback یعنی "تماس برگشتی" - یعنی یه تابع رو پاس میدیم که وقتی کار تمام شد، اون تابع رو صدا بزنه.

چطور کار می‌کنه؟

// یه تابع ساده برای گرفتن اطلاعات کاربر
function getUserData(userId, callback) {
    console.log('در حال دریافت اطلاعات کاربر...');
    
    // شبیه‌سازی عملیات زمان‌بر
    setTimeout(() => {
        const user = {
            id: userId,
            name: 'داریوش',
            age: 25
        };
        callback(user); // وقتی اطلاعات آماده شد، تابع callback رو صدا می‌زنیم
    }, 2000); // 2 ثانیه تأخیر
}

// استفاده از تابع
getUserData(123, function(user) {
    console.log('اطلاعات کاربر دریافت شد:', user);
});

console.log('این خط زودتر اجرا میشه!');

خروجی کد بالا:

در حال دریافت اطلاعات کاربر...
این خط زودتر اجرا میشه!
(بعد از 2 ثانیه) اطلاعات کاربر دریافت شد: {id: 123, name: 'داریوش', age: 25}

مشکل Callback (جهنم Callbackها)

همه چی تا اینجا خوب به نظر می‌رسه، اما مشکل کجاست؟ وقتی چندتا عملیات غیرهمزمان داریم که باید پشت سر هم اجرا بشن:

// فرض کنید می‌خوایم:
// 1. اول کاربر رو بگیریم
// 2. سپس پست‌هاش رو بگیریم
// 3. سپس کامنت‌های اولین پست رو بگیریم

function getUser(userId, callback) {
    setTimeout(() => {
        callback({ id: userId, name: 'سامان' });
    }, 1000);
}

function getPosts(userId, callback) {
    setTimeout(() => {
        callback([{ id: 1, title: 'پست اول' }, { id: 2, title: 'پست دوم' }]);
    }, 1000);
}

function getComments(postId, callback) {
    setTimeout(() => {
        callback(['کامنت 1', 'کامنت 2']);
    }, 1000);
}

// حالا چطور این‌ها رو پشت سر هم اجرا کنیم؟
getUser(123, function(user) {
    console.log('کاربر:', user);
    
    getPosts(user.id, function(posts) {
        console.log('پست‌ها:', posts);
        
        getComments(posts[0].id, function(comments) {
            console.log('کامنت‌ها:', comments);
            
            // و اگر بیشتر هم بشه...
            // getLikes(comments[0].id, function(likes) { ... })
        });
    });
});

این کد چندتا مشکل داره:

  1. خوانایی پایین: وقتی چند عملیات غیرهمزمان پشت سر هم باشن، کد گیج‌کننده و شلوغ می‌شه.

  2. مدیریت خطا سخت: باید برای هر Callback خطا رو جدا بررسی کنیم.

  3. کنترل سخت: وقتی بخوایم چند عملیات همزمان انجام بدیم و منتظر همه‌شون بمونیم، کار پیچیده می‌شه.

۲. Promise: ناجی از راه رسید!

برای حل مشکلات Callback، در ES6 (سال 2015) مفهوم Promise (قول) معرفی شد.

Promise چیه؟

Promise یه شیء (Object) هست که نماینده‌ی یه عملیات غیرهمزمانه و به ما قول میده که در آینده یا نتیجه‌ی عملیات رو بهمون میده یا دلیل شکستش رو.

سه حالت Promise:

  1. در انتظار (Pending): عملیات هنوز تموم نشده

  2. موفق (Fulfilled): عملیات با موفقیت تموم شد

  3. رد شده (Rejected): عملیات با شکست مواجه شد

ساختار Promise:

const myPromise = new Promise((resolve, reject) => {
    // اینجا عملیات زمان‌برمون رو انجام می‌دیم
    
    const success = true; // فرض کنید این نتیجه عملیات مون هست
    
    if (success) {
        resolve('عملیات موفق بود!'); // اگر موفق بود
    } else {
        reject('عملیات ناموفق بود!'); // اگر شکست خورد
    }
});

استفاده از Promise:

function getUserData(userId) {
    return new Promise((resolve, reject) => {
        console.log('در حال دریافت اطلاعات کاربر...');
        
        setTimeout(() => {
            const success = Math.random() > 0.2; // 80% احتمال موفقیت
            
            if (success) {
                const user = {
                    id: userId,
                    name: 'سارا',
                    age: 30
                };
                resolve(user); // موفق
            } else {
                reject('خطا در دریافت اطلاعات کاربر'); // شکست
            }
        }, 2000);
    });
}

// استفاده
getUserData(456)
    .then((user) => {
        console.log('اطلاعات دریافت شد:', user);
        return user.name; // می‌تونیم مقدار رو به then بعدی پاس بدیم
    })
    .then((userName) => {
        console.log('نام کاربر:', userName);
    })
    .catch((error) => {
        console.error('خطا:', error);
    })
    .finally(() => {
        console.log('عملیات تموم شد (چه موفق چه ناموفق)');
    });

چند عملیات غیرهمزمان پشت سر هم با Promise:

حالا بیایم مشکل قبلی (دریافت کاربر → پست‌ها → کامنت‌ها) رو با Promise حل کنیم:

function getUser(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ id: userId, name: 'سامان' });
        }, 1000);
    });
}

function getPosts(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([{ id: 1, title: 'پست اول' }, { id: 2, title: 'پست دوم' }]);
        }, 1000);
    });
}

function getComments(postId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(['کامنت 1', 'کامنت 2']);
        }, 1000);
    });
}

// حالا به راحتی و زیبایی:
getUser(123)
    .then(user => {
        console.log('کاربر:', user);
        return getPosts(user.id); // به مرحله بعد پاس می‌ده
    })
    .then(posts => {
        console.log('پست‌ها:', posts);
        return getComments(posts[0].id); // به مرحله بعد پاس می‌ده
    })
    .then(comments => {
        console.log('کامنت‌ها:', comments);
    })
    .catch(error => {
        console.error('خطا:', error); // فقط یه بار خطا رو مدیریت می‌کنیم
    });

چند نکته کاربردی درباره Promise:

۱. اجرای همزمان چند Promise

// فرض کنید می‌خوایم اطلاعات چند کاربر رو همزمان بگیریم
const promise1 = getUser(1);
const promise2 = getUser(2);
const promise3 = getUser(3);

// منتظر می‌مونیم تا همه‌شون تموم بشن
Promise.all([promise1, promise2, promise3])
    .then(users => {
        console.log('همه کاربران:', users);
    })
    .catch(error => {
        console.error('یکی از درخواست‌ها شکست خورد:', error);
    });

// یا اگر بخوایم اولی که تموم شد رو بگیریم
Promise.race([promise1, promise2, promise3])
    .then(firstUser => {
        console.log('اولین کاربر:', firstUser);
    });

۲. Promise.allSettled

اگر بخوایم بدون در نظر گرفتن موفقیت یا شکست، نتایج همه رو بگیریم:

Promise.allSettled([promise1, promise2, promise3])
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`Promise ${index + 1}: موفق`, result.value);
            } else {
                console.log(`Promise ${index + 1}: شکست`, result.reason);
            }
        });
    });

اما هنوز یه مشکل کوچیک...

با اینکه Promise خیلی بهتر از Callback بود، اما بازم برای بعضی‌ها سینتکس .then() و .catch() کمی غیرطبیعی به نظر می‌رسید. مخصوصاً وقتی چند مرحله از عملیات غیرهمزمان پشت سر هم اجرا می‌شدن..

۳. async/await: سینتکس/نگارش ساده و خوانا

در ES2017، دو کلمه کلیدی جدید معرفی شد: async و await که کارو خیلی ساده‌تر کردن!

async چیه؟

تابعی که با async تعریف بشه، همیشه یه Promise برمی‌گردونه:

async function sayHello() {
    return 'سلام';
}

// معادل:
function sayHello() {
    return Promise.resolve('سلام');
}

await چیه؟

await فقط داخل توابع async کار می‌کنه و منتظر میمونه تا Promise resolve بشه:

async function getUserData() {
    console.log('شروع دریافت اطلاعات...');
    
    // منتظر می‌مونیم تا Promise resolve بشه
    const user = await getUser(123);
    console.log('کاربر:', user);
    
    const posts = await getPosts(user.id);
    console.log('پست‌ها:', posts);
    
    const comments = await getComments(posts[0].id);
    console.log('کامنت‌ها:', comments);
    
    return comments; // این تابع خودش یه Promise برمی‌گردونه
}

// استفاده
getUserData()
    .then(comments => {
        console.log('نتیجه نهایی:', comments);
    })
    .catch(error => {
        console.error('خطا:', error);
    });

مدیریت خطا با try/catch:

یکی از ویژگی‌های خوب async/await اینه که می‌تونیم از try/catch معمولی استفاده کنیم:

async function getUserDataSafe() {
    try {
        const user = await getUser(123);
        const posts = await getPosts(user.id);
        const comments = await getComments(posts[0].id);
        
        console.log('همه چی اوکیه!');
        return { user, posts, comments };
        
    } catch (error) {
        console.error('یه خطایی رخ داد:', error);
        // می‌تونیم خطا رو مدیریت کنیم یا دوباره throw کنیم
        throw new Error(`خطا در دریافت اطلاعات: ${error}`);
        
    } finally {
        console.log('عملیات تموم شد');
    }
}

چند نکته کاربردی درباره async/await:

۱. اجرای همزمان

async function getAllData() {
    // این‌طوری نکنید (نوبتی اجرا میشه):
    // const user = await getUser(1);
    // const posts = await getPosts(2);
    // 3 ثانیه طول می‌کشه
    
    // این‌طوری انجام بدید (همزمان):
    const [user, posts] = await Promise.all([
        getUser(1),
        getPosts(2)
    ]);
    // 2 ثانیه طول می‌کشه (همزمان اجرا می‌شن)
    
    return { user, posts };
}

۲. حلقه با async/await

async function getMultipleUsers(userIds) {
    const users = [];
    
    // این‌طوری نکنید (نوبتی):
    for (const id of userIds) {
        const user = await getUser(id); // هر بار منتظر می‌مونه
        users.push(user);
    }
    
    // این‌طوری بهتره (همزمان):
    const userPromises = userIds.map(id => getUser(id));
    const users = await Promise.all(userPromises);
    
    return users;
}

۳. ترکیب با Promise قدیمی

// می‌تونیم از async/await با Promiseهای قدیمی هم استفاده کنیم
async function mixedExample() {
    // با async/await
    const user = await getUser(1);
    
    // با Promise قدیمی
    getPosts(user.id)
        .then(posts => {
            console.log('پست‌ها:', posts);
        })
        .catch(error => {
            console.error('خطا:', error);
        });
    
    return user;
}

بهترین روش برای شروع کدومه؟

  1. اگر تازه‌کار هستید: مستقیم برید سراغ async/await - ساده‌تر و قابل‌فهم‌تره

  2. اگر با کد قدیمی کار می‌کنید: Promise رو خوب یاد بگیرید

  3. اگر کد قدیمی دارید: فقط Callback رو بفهمید که بتونید کد قدیمی رو بخونید

مثال کاربردی نهایی:

بیایم یه مثال واقعی از فراخوانی API ببینیم:

async function fetchUserData() {
    try {
        console.log('در حال دریافت اطلاعات...');
        
        // اطلاعات کاربر
        const userResponse = await fetch('https://api.example.com/user/123');
        if (!userResponse.ok) throw new Error('خطا در دریافت کاربر');
        const user = await userResponse.json();
        
        // پست‌های کاربر (به صورت همزمان با اطلاعات اضافی)
        const [postsResponse, profileResponse] = await Promise.all([
            fetch(`https://api.example.com/user/${user.id}/posts`),
            fetch(`https://api.example.com/user/${user.id}/profile`)
        ]);
        
        if (!postsResponse.ok) throw new Error('خطا در دریافت پست‌ها');
        if (!profileResponse.ok) throw new Error('خطا در دریافت پروفایل');
        
        const posts = await postsResponse.json();
        const profile = await profileResponse.json();
        
        // پردازش اطلاعات
        const result = {
            user: user,
            postCount: posts.length,
            lastPost: posts[0],
            profile: profile
        };
        
        console.log('اطلاعات با موفقیت دریافت شد:', result);
        return result;
        
    } catch (error) {
        console.error('خطا در دریافت اطلاعات:', error);
        // می‌تونیم خطا رو به بخش دیگه‌ای پاس بدیم
        throw error;
        
    } finally {
        console.log('درخواست API تموم شد');
    }
}

// استفاده
fetchUserData()
    .then(data => {
        // انجام کار با داده‌ها
        updateUI(data);
    })
    .catch(error => {
        // نمایش خطا به کاربر
        showError(error.message);
    });

نکات طلایی:

  1. همیشه خطاها رو مدیریت کنید - چه با .catch() چه با try/catch

  2. برای عملیات مستقل از Promise.all استفاده کنید - سرعت برنامه زیاد میشه

  3. async/await جادو نمی‌کنه - فقط کد خواناتر میشه، پشت صحنه همون Promiseها هستن.

  4. تابع async همیشه Promise برمی‌گردونه - حتی اگر return عادی داشته باشه

✍️ حرف آخر

مدیریت عملیات غیرهمزمان در ابتدا ممکنه سخت به نظر برسه، اما با تمرین و درک صحیح از Callback → Promise → async/await، می‌تونید کدهای تمیز و خوانا بنویسید.

با async/await شروع کنید، درکش راحت‌تره. فقط یادتون نره که پشت صحنه دارید با Promise کار می‌کنید!

ارسال دیدگاه

دیدگاه و یا پرسش خود را برای ما ارسال کنید.

وارد شوید

برای ارسال دیدگاه یا پرسش خود ابتدا وارد سایت شوید

ورود یا ثبت نام

دیدگاه کاربران

هنوز دیدگاه یا پرسشی ایجاد نشده است :/

تازه‌ترین نوشته‌ها

دیدن همه

تجربه‌ها، دیدگاه‌ها و نکات الهام‌بخشی که با شما به اشتراک می‌گذاریم.