چرا با وجود Callback در جاوا اسکریپت به Promise نیاز داریم ؟


جاوا اسکریپت اغلب برای برنامه نویسی Asynchronous یا برنامه نویسی در حالتی که از callbackها استفاده میشه به کار میره. در مورد اینکه Callback چیه و چرا باید از اون استفاده کنیم در مقاله قبلی صحبت کردیم و مثال زدیم. اگر دقیقا نمیدونید callback چیه و چرا معرفی شد حتما قبلش اون مقاله رو بخونید.

مشکل Callback چیه؟

استفاده پیوسته از Callback ممکنه ما رو به مشکلی به نام callback hell هدایت کنه. Callback hell با توجه به سطوح مختلف تورفتگیش باعث میشه کد شما مثه یک هرم خواندش مشکل باشه که به اون Pyramid Of Doom هم میگن. خوب تو این مقاله قصدم اینه یه نگاهی به این مشکل بصورت دقیق تر داشته باشم و علت معرفی promise و اینکه این مشکل رو چطور حل می کنه بپردازم.

لازمه بگم که هر امکان جدیدی که معرفی میشه یه دلیلی پشتشه و فهم اون بجای صرفا یادگیری نحوه استفاده، درک دقیقتر و عمیقتر و البته لذت بیشتری در برنامه نویسی بهتون میده.

خوب بریم سراغ مشکلی که callback میتونه ایجاد کنه .

بیایید به حالتی فکر کنیم که احتمالاً می تونه callback hell ایجاد کنه. تصور کنیم که می خوایم اطلاعات یک کاربرو از API یا دیتابیس بگیریم. بعد از اینکه اطلاعات کاربر رو گرفتیم، می خوایم همه پست های وبلاگ اون کاربرو دریافت کنیم و بعدش وقتی که پست های وبلاگ دریافت شد ، نظرات اون پست رو هم دریافت کنیم.

در روش synchronous یا همزمان ، همه چی شسته رفته است یعنی همه چی خوب و به ترتیب و پشت سر هم اجرا میشن البته این بشرطیه که بین درخواستامون تاخیری نباشه که متاسفانه هست و سرور ممکنه تاخیر داشته باشه خوب ناچارا پس باید بگیم که از روش asynchronous یا ناهمزمان استفاده باید بشه.

بنابراین اول بیایین سه تا function ایجاد کنیم که این سه تا کار و انجام میده.

یعنی گرفتن کاربر، گرفتن پست ها، گرفتن کامنت ها

function getUser(id, callbackfunc) {
    setTimeout(() => {
        console.log('Getting the user from the database...');
        callbackfunc({
            id: id,
            name: 'Vegibit'
        });
    }, 1000);
}
function getBlogPosts(username, callbackfunc) {
    setTimeout(() => {
        console.log('Calling WordPress Rest API for posts');
        callbackfunc(['Post1', 'post2', 'post3']);
    }, 1000);
} 
function getComments(post, callbackfunc) {
    setTimeout(() => {
        console.log('Calling WordPress Rest API for comments for ' + post);
        callbackfunc(['comments for ' + post]);
    }, 1000);
}

خوب سه تا تابع نوشتیم که هر کدوم حدود یه ثانیه تاخیر ایجاد میکنن بعد دیتایی که میخاییم رو برمیگردونن. حالا باید اینها رو به ترتیب کال کنیم یعنی اول یوز رو بگیریم بعد از اینکه اطلاعت یوزر برگشت تابع دوم پستاش و بعد کامنت هاش ....

پس اینجوری مینویسمش :

getUser(1, (user) => {
    getBlogPosts(user.name, (blogposts) => {
        getComments(blogposts[0], (comments) => {
            console.log(user, blogposts[0], comments);
        })
    })
});

خوب به نظر همه چی درسته !

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

بلی این کد کار می کنه، خوب پس !!! اگر کار میکنه دیگه مقاله نوشتنوتون چی بود؟؟ نکته اینکه که خوندن این دست کده ها و نگاه کردن بهش آسون نیست. از این بدتر ، اگر سطوح بیشتری از callback داشته باشیم تورفتگی بیشتر میشه و مطمئناً در کد گم می شیم (به بالای 5 تا کالبک تو در تو لحظه ای فکر کنید یا تصویر پایین رو نگاه کنید) ، این خودش میتونه سبب ایجاد باگ بشه که خوب بعدا براتون دردسر ساز میشه. حالا پیدا کردن باگ هم خودش داستانیه، کد پایین ببینید اینجا همون چیزیه که به اون Pyramid Of Doom میگیم! یا همون جهنم callback معروف


خوب چه راهکارایی هست از شر این کال بک راحت شیم ؟

اول از همه که خوب اگر یک یا دو سطح کال بک دارین سخت نیست نیازی هم نیست کاری کنید.
دوم یه روش که این مشکل رو کمتر میکنه استفاده از Named functions هست ولی روش پرفکتی نیست از حوصله این مقاله هم خارجه خواستین در موردش مطالعه کنید برین اینجا

میریم سراغ اصل مطلب اینکه از Promise ها استفاده کنیم و اینکه چطور بهمون در این موضوع کمک میکنن.

معرفی آقای Promise

حالا بهش گفتم آقا، خانوما نارحت نشن چون آقایون زیاد قول میدن گفتیم اینجوری صداش کردم.

پرامیس ها یا Promises (وعده ها :-/ از من به شما نصحیت همون انگلیسشو یاد بگیرید) نتیجه نهایی یک عملیات asynchronous رو برامون نگه میدارن. بطور کلی هر وقت عملیات ناهمزمان تموم بشه میتونه منجر به یه نتیجه یا یک خطا بشه یعنی چی ؟ جلوتر میگم ، بطور کلی یه promise سه حالت اصلی میتونه داشته باشه اول Pending بعد Fulfilled و آخری Rejected. خوب حالا یعنی چی؟ همون مثال بالا رو در نظر بگیریم که میخاییم کاربر رو از دیتابیس بگیریم وقتی درخواست میدیم promise میره تو فاز pending بعدش که نتیجه اومد میره تو حالت fulfilled اگر هم نتوست و مشکلی پیش اومد اینترنت قطع شد سرور اتیش گرفت بد قول بود یا هرچی میشه rejected

نحوه نوشتن یک پرامیس خیلی راحت هست. ابتدا با ساختار اون بشیم و بعد مثال بالا رو با پرامیس خواهیم نوشت:

var promise = new Promise(function(resolve, reject) {
    // do something ...
})

همونطور که می‌بینیم، پرامیس یک آبجکت از کلاس ‌Promise هست که فقط یک آرگومان می‌گیره که یک تابع بی‌نام هست. وقتی یک پرامیس ساخته میشه، این تابع بطور خودکار اجرا میشه. این تابع عملیات مورد نظر رو انجام میده و نهایتا باید یک نتیجه رو به خروجی بفرسته. نتیجه موفقیت‌آمیز بودن یا خطا. این تابع دو تا آرگومان می‌گیره که اولی resolve و دومی reject هست.


وقتی عملیات این تابع به پایان رسید، اگر خروجی کار موفقیت‌آمیز بود، باید resolve رو صدا بزنیم و اگر خطا داشت reject رو صدا می‌زنیم.

بزارین یه مثال خیالی عملی تر بزنم.

const promise = new Promise((resolve, reject) => {
    // Do some asynchronous tasks
    // ...
    setTimeout(() => {
        let apiResponse = 1;
        if (apiResponse == 1) {
            resolve('It worked'); // pending => resolved, fulfilled
        } else {
            reject(new Error('Something went wrong')); // pending => rejected
        }
   }, 1000);
});
promise.then(result => console.log('Result:', result))
    .catch(err => console.log('Error', err.message));


زمانی که بخش resolve اجرا میشه یا عملیات موفق هست (apiResponse = 1)

و زمانی که درخواست بخش rejected اجرا میشه یا عملیات نا موفق هست (apiResponse=0) بخش catch پرامیس یه خطا نشون میده

خوب الان با نحوه کار پرامیس میتونیم مثال بالامون رو با کمک پرامیس دوباره بنویسیم .

پرامیسس به جای کال بک

در مثالی که برای callback hell نشون دادم ساختار parymid of doom وجود داشت، به عبارتی ، ما اونجا مشکل تو در تو بودن کد رو داشتیم که خواندن کد رو دشوار میکرد. در اینجا ، می تونیم توابع asynchronous را طوری تغییر بدیم که یک promise برگردونن. از اونجایی که این توابع async یک promise برمی گردونن پس می تونیم با استفاده از .then () و .catch () این promise ها رو بکار بگیریم و بدینگونه Parymid of doom و callback hell را حذف کنیم.

توجه : توابع asynchronous از تابع بعنوان پارامتر دیگه استفاده نمیکنن در واقع خودشو یه promise به ما return میکنن. امیدوارم مشخص شود که در داخل وعده بازگشت شده ، اکنون کد async (خواندن یک پایگاه داده یا درخواست API) قرار دارد.

// getUser(1, (user) => {
//     getBlogPosts(user.name, (blogposts) => {
//         getComments(blogposts[0], (comments) => {
//             console.log(user, blogposts[0], comments);
//         })
//     })
// }); 
getUser(1)
    .then(user => getBlogPosts(user.name))
    .then(blogposts => getComments(blogposts[0]))
    .then(comments => console.log(comments))
    .catch(err => console.log('Error: ', err.message));
 
function getUser(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('Getting the user from the database...');
            resolve({
                id: id,
                name: 'Vegibit'
            });
        }, 1000);
    });
}
function getBlogPosts(username) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('Calling WordPress Rest API for posts');
            resolve(['Post1', 'post2', 'post3']);
        }, 1000);
    });
}
function getComments(post) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('Calling WordPress Rest API for comments for ' + post);
            resolve(['comments for ' + post]);
        }, 1000);
    });
} 


خوب اینم از پرامیس کدشو سعی کنید خودتون یه بار بنویسید تو سایتایی مثه playcode.io ، codesandbox یا مروگر و...این کد رو برین تست کنید باهاش کار کنید. تا واستون جا بیفته.


خلاصه مزایای پرامیس‌ها

  • خوانایی کد بالاتر میره. چون کدهای ساده‌تری خواهیم داشت.
  • مدیریت بهینه‌تر نتیجه موفقیت‌آمیز یک عملیات ناهمگام
  • مدیریت بهینه‌تر خطای یک عملیات ناهمگام

اوکی مقاله طولانی شد. اگرچه زیاد توضیح دادم ولی میدونم که تا الان به این خوبی کسی پرامیس رو توضیح نداده بود ;-) خوشحالم که خوشتون اومده، کامنت و لایک فراموش نشه حتما واسه دوستا و هم تیمیاتون که با پرامیس و کال بک مشکل دارن بفرستن این مقاله رو و تا میتونید شیر کنید.

مقاله بعدی که مخام بنویسیم راجبع اینه که چرا با وجود callback و promise تو نسخه es8 اومدن و async await رو معرفی کردن چه کاربردی داره و چه مشکلی رو حل میکنه ، یه مقاله هم راجبع کارایی که با پرامیس میشه انجام داد خواهم نوشت پس همراهم باشید.

ممنون که مقاله رو خوندید.

حمید شجاع


<br/>https://vegibit.com/javascript-callbacks-vs-promises-vs-async-await/