یکی از مهم ترین ویژگی هایی که جاوا اسکریپت رو به زبانی محبوب و پرکاربرد تبدیل کرده بحث non-blocking عه. برای اینکه این مفهوم جا بیافته لازمه نحوه load شدن صفحات جاوا اسکریپتی رو بیشتر بررسی کنیم وقتی یه صفحه وب رو باز میکنیم بسته به نوع و اندازه اون تعداد متفاوتی کامپوننت لود میشه که بعضا ممکنه داده ها و اطلاعاتش استاتیک باشه یا بصورت داینامیک از یجای remote فراخوانی بشن خوب حالا اگه این صفحه تعداد کامپوننت های داینامیکش زیاد باشه یعنی قرار باشه هر کدوم داده هاشو از یه سرویس یا سروری لود کنه صفحه از بالا تا پایین به ترتیب این فراخوانی ها انجام میشه و بعد اینکه با موفقیت داده ها فراخوانی شد تو قسمت مربوطه نشون داده میشن.
فرض کنیم تمام سرویسایی که برای ارسال و دریافت داده ها اجرا می شن بصورت synchronous باشند یعنی هر درخواست باید تکمیل شه تا نوبت به پردازش بعدی برسه حالا اگه اون وسطا یه سرویس قطع باشه یا به هر دلیلی نتونه جواب مناسب رو بگیره رندر و نمایش صفحه همونجا متوقف میشه و اصلاحا میگن blocking اتفاق افتاده حالا برای حل این مسئله اومدن از سرویس ها Async استفاده کردن به این معنی که هر درخواست سرویسی لازم نیست صبر کنه تا جوابش بیاد کامپوننت های تو صفحه همه پشت سر هم اجرا می شن و هر وقت جواباشون اومد کامپوننت ها خودشونو بروز میکنن و مجددا render میشن اینجوری اگه سرویسی جواب نداد کل صفحه crash نمیکنه و فقط اون قسمت خاص کار نمیکنه.
برای پیاده سازی سرویس های async از سه روش callback، promise و async/await استفاده میکنن در ادامه تعریف و پیاده سازی هر کدومو انجام میدیم و تفاوت هاش و جاهایی که کاربرد داره هم بررسی میکنیم.
قبل از هرچی لازمه با مفهوم closure آشنا بشید: به توابعی که در آن ها به متغیرهای خارج از اون تابع دسترسی داشته باشیم توابع closure میگن که در واقع توابع callback و async هم از این نوع هستن. توابع closure بیشتر تمرکزشون روی scope متغیراس که اجازه دسترسی در سه حالت بهشون میده: یکی متغیرهای خوده تابع، دوم متغیرهای بیرون تابع و سوم متغیرهای global. حالا چرا لازمه دسترسی داشته باشه: فک کنید تو صفحه یه جدول داریم که اطلاعات کاربران رو نشون میده و این داده ها از طریق یه callback که سرویس فراخوانی میکنه پر میشن و از اونجایی که async هست در ابتدای لود صفحه اول جدول خالی نمایش داده میشه و بعد تو بدنه تابع callbackداده ها پر میشن و بعد از یه وقفه کوتاهی که خیلی وقتا هم ممکنه متوجه نشیم یا وقتی صفحه میره تو فاز loading... جدول با داده ها به نمایش درمیاد پس لازمه به متغیری که داده های جدول رو پر میکنه و بیرون از تابع تعریف شده برای پرکردن دسترسی داشته باشیم.
بعد از اون مفهوم promise مهمه بدونید چیه؟
در واقع object ای که ایجاد شده تا توابع async مقدار بازگشتی مثه sync داشته باشن یعنی توابعی که async باشن مثه callback وقتی صدا زده بشن یه object از نوع promise برگردونن. حالا این object چیه و چه ویژگی هایی داره؟ فرض کنید یه سرویسی ساخته شده که وقتی فراخوانی میشه به ما یه promise برمیگردونه، اول از همه مقدار برگشتی state, result داره که در ادامه میگم هرکدوم با چیا پر میشن. state سه نوع داریم: pending, fulfilled, rejected بعدا توضیح میدم چجوری تغییر میکنن. این سرویس وقتی یه object از نوع promise ایجاد میکنه تو constructor یه تابع با دو تا پارامتر مهم با نام resolve , reject پاس میده که اسم این تابع executor هست. کارشون چیه؟ اگه سرویس کاری که قرار بوده انجام بده با موفقیت انجام شده بشه نتیجه کار سرویس که حالا ممکنه لیستی از کاربرا باشه تو resolve ست میشه و اگه اجرای سرویس به خطا خورد یه object از نوع error مدنظر تو reject ست میشه. دقت داشته باشیم resolve , reject جزو توابع از پیش تعریف شده جاوا اسکریپت هستن و خودشون callback تعریف شدن.
let myPromise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve("Success!");
}, 250);
setTimeout(function() {
reject(new Error("Success!"));
}, 250);
});
همونجوری که میبینید مقادیر تو تابع پر شدن البته باید توجه کنیم که هر promise فقط یکی از اینا رو برمیگردونه مثلا اگه resolve پر شده بود و اوکی بود همینجا بر میگرده و بقیه ignore میشن.
وضعیت در ابتدا بصورت pending و اگه resolve شد میره به fulfilled اگرم خطا داشت و reject شد میره به وضعیت rejected.
شکل زیر وضعیت و نتیجه را کامل نشون میده
اگه قبلا از jQuery استفاده کرده باشیم برای فراخوانی سرویس ریموت از ajax استفاده میکردیم که در واقع یه callback بود و حالا تو جواب برگشتی یه کارایی انجام میدادیم. این روزا که اکثرا از فریمورک جاوا اسکریپتی استفاده میکنن یه کتابخانه قوی برای فراخوانی سرویسای rest با نام axios ایجاد شده که همه http method ها رو ساپورت میکنه و در واقع یه async هست که بسته به پیاده سازیمون میتونه promise یا async/await باشه. من چون از react برای پیاده سازی بخش front استفاده میکنم مثالارو از اون میارم و درکل فرقی با بقیه نمیکنه. axios در حقیقت بگونه ای طراحی شده که callback هایی را پیاده سازی می کند که promise برمی گرداند.
یه مثال ساده از فراخوانی async با axios:
getUsers = () => {
axios
.get("http://localhost/api/users")
.then(response => setUsersState(response))
.catch(err => {
console.log(err);
});
};
برای مدیریت response برگشتی اگه success باشه تو then و اگه fail باشه توی catch مدیریت میشه. بر فرض که سرویس بالا لیست کاربرا رو بما بده تو یه متغیر با استفاده از useState ست میکنیم و در جایی از صفحه این usersState رو بصورت جدول میتونیم نمایش بدیم.
همین عملیات بالا رو با async/await هم میتونیم پیاده سازی کنیم ولی چرا؟ مگه چه مشکلی داره؟
در حال حاضر async/await جزو قابلیت هایی که توی ES6 معرفی شده و برای رفع پیچیدگی callback hell ایجاده شده. اول همین مثال بالا رو با async/await پیاده سازی کنیم بعد میریم سراغ پیچیدگی.
getUsers = async () => {
try {
let response = await axios.get("http://localhost/users");
setUsersState(response.data);
} catch (e) {
console.log(e);
}
};
نکته مهمی که اینجا وجود داره اینه که await باید حتما توی تابعی که با async تونسته wrap بشه استفاده میشه. نکته بعدی اینه که وقتی کلمه await را استفاده میکنیم به این معنیه که صبر میکنه تا جواب فراخوانی برگرده بعد ادامه میده یعنی response که resolve آن را پر کرده برای خط های بعدی حتما مقدار داره و catch هم اگر reject شد. در اینجا باید متذکر بشم که چون await داخل async قرار داره باعث ایجاد blocking نمیشه.
حالا اگه شرایطی پیش بیاد که مجبور شیم از callback chain استفاده کنیم یعنی فراخوانی های async تو در تو داشته باشیم بطوریکه نتیجه یکی به عنوان ورودی در فراخوانی بعدی باشد پیچیدگی مدیریت resolve , reject کارو سخت میکنه که نتیجش میشه callback hell حالا مدیریت این زنجیره با asynch/await راحت تره.
به مثال زیر دقت کنید:
getUsers = () => {
axios
.get("http://localhost/api/users")
.then(response => {
return axios.post("http://localhost/roles", response.data);
})
.then(response => setUsersState(response))
.catch(err => {
console.log(err);
});
};
همینو میتونیم با async/await پیاده سازی کنیم:
getUsers = async () => {
try {
let response = await axios.get("http://localhost/users");
let res = await axios.post("http://localhost/roles",response.data);
setUsersState(res);
} catch (e) {
console.log(e);
}
};