شاید شما هم با مفهوم async و await در جاوا اسکریپت آشنا نباشید و ندانید که این ویژگیهای برای چه به جاوا اسکریپت اضافه شدند. همانطور که میدانید جاوا اسکریپت یک زبان تک رشتهای است و امکان چندوظیفهای در این زبان وجود ندارد؛ بنابراین وظایف مختلف در این زبان نمیتوانند به صورت همزمان اجرا شوند و باید از راهکارهای موجود در این زبان برای همگام سازی وظایف استفاده کرد. callback ها یکی از راهحلهایی بودند که جاوا اسکریپت برای یادآوری وظایف در زمان معین استفاده میکرد؛ ولی این روش سختیهای خاص خود را داشت و همیشه درست جواب نمیداد. بعد از مدتی promise ها جای خود را در این زبان باز کردند و توانستند به شکل منضبطتر و هماهنگتری فعالیتهای همزمان را مدیریت کنند. Async await در جاوا اسکریپت قابلیت جدیدی است که طی چند سال اخیر به این زبان افزوده شده و کار کردن با توابع ناهمگام را راحتتر کرده است. در این مقاله قصد داریم به بررسی Async await در جاوا اسکریپت بپردازیم و ببینیم که این افزونه چطور مشکل ناهمگامی توابع را حل کرده است.
Async و await در جاوا اسکریپت
همانطور که گفته شد در جاوا اسکریپت multitask وجود ندارد؛ بنابراین توابع باید پشت سر هم اجرا شوند. فرآیندهای مختلفی وجود دارند که اجرای آنها منوط به اجرای فرآیندهای قبلی است؛ مثلا برای محاسبهی میانگین چند عدد ابتدا باید این اعداد با هم جمع شوند و خروجی آن بر تعداد اعداد تقسیم شود. برای میانگین گیری شما نمیتوانید ابتدا عمل تقسیم را انجام دهید و سپس اعداد را با هم جمع کنید؛ چون به خروجی جمع برای تقسیم نیاز خواهید داشت. برای درک بهتر توابع آسنکرون به تصویر زیر دقت کنید.
کاری که باید انجام شود این است که توابع با هم همگام شوند و ترتیب اجرای آنها مشخص شود. این دقیقا همان کاری است که افزونهی Async و await در جاوا اسکریپت برای ما انجام میدهند. این افزونه بر اساس promise ها کار میکند. پس بهتر است قبل از پرداختن به مفهوم async و await در جاوا اسکریپت، promise ها را معرفی کنیم.
Promise در جاوا اسکریپت
Promise در جاوا اسکریپت به معنی عملیات غیرمتقارن است؛ یعنی عملیاتی که برای اجرا باید منتظر اجرای عملیات دیگری باشند. در واقع پرامیس یک شی نگهدارنده است که تابعی را به عنوان ورودی دریافت کرده و پس از اتمام اجرای آن، با یک تابع callback، فراخوانی میشود. یک پرامیس میتوانید در یکی از سه وضعیت قرار بگیرد:
Pending: وضعیت انتظار نیز گفته میشود و زمانی است که کدهای تابع هنوز به طور کامل اجرا نشدهاند.
Fulfilled: وضعیتی است که کدها به صورت کامل اجرا شده و تابع تکمیل شده است.
Rejected: در این وضعیت اجرای تابع بنا به دلایلی با شکست مواجه شده و تابع اجرا نشده است.
کد زیر نمونهای از ساخت یک پرامیس در جاوا اسکریپت را نشان میدهد.
const p = new Promise(function(resolve , reject){
// کدهای تابع که معمولاً به صورت آسنکرون اجرا میشوند
if(success){
resolve(value);
}else{
reject(error);
}
});
در اینجا پرامیس از نوع شی تعریف شده که آرگومان ورودی آن یک تابع است. تابع function خود دو پارامتر ورودی با نامهای resolve و reject دارد که هر کدام وضعیتهای زیر را نشان میدهند:
Resolve: اجرای موفقیت آمیز عملیات آسنکرون که به آن fulfilled نیز گفته میشود.
Reject: شکست عملیات آسنکرون
اگر تابع در هر کدام از دو وضعیت بالا قرار گرفته باشد، اجرای متوقف شده و در نتیجه عمر پرامیس تعریف شده برای آن نیز به پایان میرسد.
async در جاوا اسکریپت
async در جاوا اسکریپت برای توابع آسنکرون استفاده میشود. این ویژگی به صورت خودکار یک پرامیس برای تابع میسازد تا اجرای آن را مدیریت کند. نحوهی ساخت یک async به شکل زیر است:
async function f() {
return 1;
}
این دستور باید قبل از دستور فانکشن نوشته شود و یک معنی ساده دارد؛ تابع همیشه یک پرامیس برمیگرداند. در صورتی که پرامیس به وضعیت تکمیل یا همان resolve برود، تابع مقدار یک را برمیگرداند. این کد را در نظر بگیرید:
async function f() {
return 1;
}
f().then(alert); // 1
این کد را میتوان به این صورت نیز نوشت:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
بنابراین تابع async در جاوا اسکریپت تضمین میکند که یک پرامیس باید برگردانده شود؛ مگر اینکه اجرای آن با شکست مواجه شود. اما این تمام ماجرا نیست، دستور دیگری نیز وجود دارد که پس از آسینک اجرا میشود و Await نام دارد.
await در جاوا اسکریپت
به بیان ساده Await در جاوا اسکریپت اجرای دستور Async را متوقف میکند. اگر Async نباشد، Await ای هم وجود نخواهد داشت. وقتی بعد از یک پرامیس از Await استفاده میشود، این دستور اجرای مابقی کدها را تا زمان تکمیل پرامیس متوقف میکند. میتواند گفت عملکرد دو مفهوم async و await در جاوا اسکریپت مکمل یکدیگرند. بد نیست بدانید تابع Await در جاوا اسکریپت فقط با پرومیسها کار میکند و کاری به callbackها ندارد. سینتکس تابع await در جاوا اسکریپت به صورت زیر است:
// works only inside async functions
let value = await promise;
در نمونهی زیر یک پرامیس داریم که در عرض یک ثانیه تکمیل میشود.
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
اجرای تابع بالا در خط کد (*) به حالت توقف میرود و زمانی مجددا اجرای کدها را ادامه میدهد که پرامیس به وضعیت resolve یا تکمیل رفته باشد. در نتیجه خروجی کد در یک ثانیه عبارت done! را نمایش خواهد داد. همانطور که گفته شد، await در جاوا اسکریپت، اجرای توابع را تا زمان تکمیل پرامیس متوقف میکند و بعد از کامل شدن تابع داخل پرامیس، اجرای کد را مجددا از سر میگیرد. اما، به این نکته توجه داشته باشید که این کار هیچگونه اتلاف cpu ندارد؛ چون موتور جاوا اسکریپت در این زمان میتوان به اجرای کارهای دیگری نظیر اجرای اسکریپتهای دیگر، رسیدگی به ایونتها و… بپردازد.
مثال async await در جاوا اسکریپت
برای درک بهتر مفهوم async و await در جاوا اسکریپت یک مثال از کاربرد این افزونه در جاوا اسکریپت میزنیم تا شما بتوانید سادگی کار با این دو ویژگی را با ویژگیهای promise و callback مقایسه کنید. قطعه کد زیر نحوهی دریافت یک منبع JSON و تفکیک آن را نشان میدهد:
const getFirstUserData = () => {
return fetch('/users.json') // get users list
.then(response => response.json()) // parse JSON
.then(users => users[]) // pick first user
.then(user => fetch(`/users/${user.name}`)) // get user data
.then(userResponse => userResponse.json()) // parse JSON
}
getFirstUserData()
کد زیر همان تابع را با استفاده از async await در جاوا اسکریپت بازنویسی میکند:
const getFirstUserData = async () => {
const response = await fetch('/users.json') // get users list
const users = await response.json() // parse JSON
const user = users[] // pick first user
const userResponse = await fetch(`/users/${user.name}`) // get user data
const userData = await userResponse.json() // parse JSON
return userData
}
getFirstUserData()
همانطور که ملاحظه میکنید، استفاده از async await میتواند کدهای ما را سادهتر کند. نکتهی آخر از این مبحث اینکه توابع آسنکرون را میتوان به راحتی پشت سر هم زنجیر کرد و سینتکسشان نیز بسیار سادهتر از زنجیرههای پرامیس خواهد بود. به مثال زیر توجه کنید:
const promiseToDoSomething = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}
const watchOverSomeoneDoingSomething = async () => {
const something = await promiseToDoSomething()
return something + '\nand I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
const something = await watchOverSomeoneDoingSomething()
return something + '\nand I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
console.log(res)
})
مدیریت ارورها در Async Await
مدیریت ارورها یکی دیگر از قابلیتهای افزونهی Async Await است که به شما این امکان را میدهد هر گونه خطایی را به داخل ساختار try…catch ارسال کنید. به عنوان مثال:
function thisThrows() {
throw new Error("Thrown from thisThrows()");
}
try {
thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}
// Output:
// Error: Thrown from thisThrows()
// ...stacktrace
// We do cleanup here
کد بالا یک دستور سادهی try… catch را نشان میدهد که در آن تابع thisThrows() یک خطا را throw کرده و ما آن را catch میکنیم و در بلوک finally یک کد اختیاری برای آن اجرا میکنیم. این تابع را میتوان با کمک مفهوم async و await در جاوا اسکریپت به شکل زیر پیاده سازی کرد:
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}
async function run() {
try {
await thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}
}
run();
// Output:
// Error: Thrown from thisThrows()
// ...stacktrace
// We do cleanup here
همانطور که ملاحظه میکنید، استفاده از async و awaitبرای اینکار کار پیاده سازی را راحتتر کرده و باعث خوانایی بیشتر و درک بهتر کد میشود.
پشتیبانی مرورگرها
Ecmascript 2017 دو کلمهی کلیدی async و await را معرفی کرد. در شکل زیر نسخههای مرورگرهایی که از این افزونه پشتیبانی میکنند را ملاحظه میکنید.
سخن پایانی
دو مفهوم async و await در جاوا اسکریپت در کنار یکدیگر چارچوبی عالی برای نوشتن و اجرای کدهای آسنکرون یا غیرهمزمان ارائه میدهند و خواندن و نوشتنشان نیز بسیار ساده است. به طور خلاصه کلمه کلیدی async دو ویژگی اصلی دارد:
همیشه یک پرامیس برمیگرداند
به await اجازه میدهد تا از آن استفاده کنید.
از سوی دیگر await قبل از اجرای کامل پرامیس به کدهای جاوا اسکریپت ایست میدهد تا مطمئن شود که:
اگر خطایی رخ داد، یک exception ایجاد شود.
و اگر پرامیس کامل شد مقدار خروجی را برگرداند.
با کمک این دو افزونه ما به ندرت نیاز به نوشتن زنجیرهی پرامیسهای پشت سر هم خواهیم داشت؛ ولی فراموش نکنید که این دو مفهوم نیز از پرامیس استفاده میکنند و گاهی ممکن است ناچار به استفاده از این زنجیرهها شویم.
سوالات متداول
آیا مفهوم async و await در جاوا اسکریپت از نوع blocking هستند؟
تنها await از نوع blocking است و اجرای کدهای جاوا اسکریپت را تا قبل از تکمیل تابع Async بلوکه میکند. این کار برای این است که مطمئن شود، هر آنچه لازم بوده اجرا شده و میتوان مابقی برنامهی نوشته را اجرا کرد.
آیا میتوان از async بدون await استفاده کرد؟
این امکان وجود دارد که شما Async را به تنهایی به کار ببرید؛ ولی باید این نکته را در نظر داشته باشید که در صورت اجرای async بدون await، کدهای شما به صورت non blocking به اجرای خود ادامه خواهند داد و تابعی برای تابع دیگر منتظر نخواهد ماند.