یک روش خاص برای کار با Promise ها به شیوه راحتتر وجود دارد که به آن “async/await” گفته می شود. فهمیدن و استفاده از آن به شکل غافلگیر کننده راحت است.
توابع Async
بیایید با کلیدواژه async
شروع کنیم. این کلیدواژه قبل از یک تابع قرار می گیرد، مانند زیر:
async function f() {
return 1;
}
وجود کلمه “async” قبل از یک تابع یک معنی ساده می دهد: تابع همیشه یک Promise برمی گرداند. سایر مقادیر به صورت خودکار با یک Promise انجام شده در بر گرفته می شوند.
برای نمونه، این تابع یک Promise انجام شده با مقدار 1
را برمی گرداند؛ بیایید امتحان کنیم:
async function f() {
return 1;
}
f().then(alert); // 1
… ما میتوانیم به طور مستقیم یک Promise را برگردانیم، که همان خواهد بود:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
بنابراین، async
تضمین می کند که تابع یک Promise برمی گرداند و قسمت های غیر Promise آن را در بر می گیرد. ساده است، نه؟ اما فقط این نیست. کلیدواژه دیگری به اسم await
وجود دارد که فقط داخل توابع async
کار می کند.
Await
به شکل زیر استفاده می شود:
// تنها در توابع async کار می کند
let value = await promise;
کلیدواژه await
باعث می شود که جاوااسکریپت تا اجرا شدن آن Promise صبر کند و مقدار آن را برگرداند.
در اینجا مثالی از یک Promise داریم که در مدت ۱ ثانیه با موفقیت اجرا می شود:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // صبر می کند تا پرامیس با موفقیت اجرا شود (*)
alert(result); // "done!"
}
f();
اجرای تابع در خط (*)
متوقف می شود و زمانی که Promise اجرا شد ادامه می یابد، به صورتی که result
نتیجه آن می شود. بنابراین قطعه کد بالا مقدار “!done” را طی یک ثانیه نمایش می دهد.
تاکید می کنیم: await
در واقع اجرای تابع را تا زمان به اتمام رسیدن اجرای Promise به تعلیق در می آورد و در ادامه با نتیجه آن اجرای تابع ادامه می یابد. این اتفاق هزینه ای برای منابع پردازشی ندارد؛ زیرا موتور جاوااسکریپت می تواند به طور همزمان کارهای دیگری مانند اجرای اسکریپت های دیگر، مدیریت سایر اتفاقات و غیره را انجام دهد.
این روش روش زیباتری برای گرفتن نتیجه Promise نسبت به promise.then
است و خواندن و نوشتن آن نیز راحت تر است.
await
در تابع عادی استفاده کرداگر تلاش کنیم تا از await
در یک تابع غیر async استفاده کنیم، خطای syntax ای وجود خواهد داشت:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
اگر فراموش کنیم که async
را قبل از تابع قرار دهیم این خطا را می گیریم. همانطور که قبلا هم گفته شد، await
فقط در تابع async
کار می کند.
بیایید مثال showAvatar()
از بخش زنجیرهای کردن Promise را با استفاده از async/await
مجدد بنویسیم:
۱. ما نیاز داریم که فراخوانی های then.
را با await
جایگزین کنیم.
۲. همچنین باید تابع را async
کنیم تا آنها کار کنند.
async function showAvatar() {
// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
بسیار تمیز و آسان برای خواندن، درسته؟ خیلی بهتر از قبل شد.
await
در سطوح بالا یک ماژول را می دهند.در مرورگر های پیشرفته، await
زمانی که داخل یک ماژول هستیم، به خوبی در سطوح بالا کار می کند. ما ماژول ها را در مقاله ماژول ها، معرفی پوشش خواهیم داد…
برای مثال:
// ما فرض می کنیم که این کد در سطح بالا در داخل یک ماژول اجرا می شود
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
اگر ما از ماژول یا مرورگر های قدیمی که این ویژگی را پشتیبانی کنند استفاده نکنیم، یک راهکار کلی وجود دارد: دربرگرفتن در یک تابع بدون نام async.
مانند زیر:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await
، “thenables” می پذیردمانند promise.then
، await
به ما این امکان را می دهد تا از thenable objects استفاده کنیم (آنهایی با متد قابل فراخوانی then
). ایده این است که object ثالث ممکن است promise نباشد اما قابل انطباق با promise باشد: اگر از then.
پشتیبانی کند، این مورد برای با await
استفاده شدن کافیست.
اینجا پیش نمایشی از یک کلاس Thenable
می بینیم که await
نمونه ای از آن را پذیرفته است:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// بعد از ۱۰۰۰ میلی ثانیه با مقدار this.num * 2 حل می شود
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// برای ۱ ثانیه صبر می کند و سپس result مقدار ۲ را می گیرد
let result = await new Thenable(1);
alert(result);
}
f();
اگر await
شیء غیر Promise ای که دارای then.
است را دریافت کند، آن متد را به طوری فراخوانی می کند که توابع resolve
و reject
به عنوان پارامتر به آن متد هنگام فراخوانی داده شده است (همانطور که برای یک اجرا شونده Promise
معمولی این کار را انجام می دهد). سپس await
صبر می کند تا یکی از آن دو فراخوانی شود (در مثال بالا، این اتفاق در خط (*)
رخ می دهد) و با مقدار result به کار خود ادامه می دهد.
برای تعریف یک متد async در کلاس، کافیست آن را async
قید کنید:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (این حالت همانند حالت رو به رو است (result => alert(result)))
منظور یکی است: این روش تضمین می کند که مقدار بازگشتی یک Promise است و استفاده از await
را امکان پذیر می کند.
مدیریت خطا
اگر یک Promise به صورت عادی اجرا شود، پس از آن await promise
نتیجه را برمی گرداند. اما در صورت رد شدن، باعث بروز خطا می شود همانند حالتی که در آن خط عبارت throw
وجود داشته است.
این کد:
async function f() {
await Promise.reject(new Error("Whoops!"));
}
… همانند کد زیر است:
async function f() {
throw new Error("Whoops!");
}
در شرایط واقعی، ممکن است Promise مدتی طول بکشد تا به خطا بخورد. در این حالت قبل از اینکه await
به بروز خطا منجر شود، تاخیری وجود دارد.
ما می توانیم آن خطا را، مانند یک throw
عادی، با استفاده از try..catch
بگیریم:
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
در شرایط بروز خطا، قسمت کنترل وارد بلوک catch
می شود. ما همچنین می توانیم چندین خط را در بر بگیریم:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// خطاها هم از fetch و هم از response.json() گرفته می شود
alert(err);
}
}
f();
اگر try..catch
نداشتیم، در این صورت با فراخوانی تابع ()f
یک Promise ساخته می شود که رد شده است. ما می توانیم برای مدیریت این حالت catch.
را به آن اضافه کنیم:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() به یک Promise رد شده تبدیل می شود
f().catch(alert); // TypeError: failed to fetch // (*)
اگر فراموش کنیم که catch.
را اضافه کنیم، در نتیجه به خطای unhandled promise error می خوریم (در کنسول قابل مشاهده است). همانطور که در بخش مدیریت ارورها با promiseها توضیح داده شد، ما میتوانیم با استفاده از یک مدیریت اتفاق unhandledrejection
کلی چنین خطاهایی را مدیریت کنیم.
async/await
و promise.then/catch
زمانی که ما از async/await
استفاده می کنیم، کمتر پیش میاید که به then.
نیاز شود؛ زیرا await
خود فرآیند متوقف شدن را مدیریت می کند. همچنین می توانیم به جای catch.
از try..catch
عادی استفاده کنیم. این کار معمولا (و نه همیشه) بسیار راحت تر است.
اما در کد سطح بالا، زمانی که ما بیرون از هر تابع async
ای هستیم، ما به دلایلی سینتکسی نمی توانیم از await
استفاده کنیم؛ بنابراین این یک کار عادی است که برای مدیریت نتیجه نهایی یا برخوردن با خطاهای احتمالی then/catch.
را اضافه کنیم، مانند خط (*)
در مثال بالا.
async/await
به خوبی با Promise.all
کار می کندزمانی که نیاز داریم تا زمان اجرای چند Promise صبر کنیم، می توانیم همه آنها را در یک Promise.all
قرار دهیم و سپس از await
استفاده کنیم:
// برای آرایه از نتیجه ها صبر می کنیم
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
در شرایط بروز خطا به صورت معمول خطا از Promise ناموفق به Promise.all
منتقل می شود و سپس به یک exception تبدیل شده که می توان با قرار دادن try..catch
آن را مدیریت کرد.
خلاصه
کلیدواژه async
قبل از یک تابع دو تاثیر می گذارد:
۱. کاری می کند که همیشه یک Promise برگرداند.
۲. اجازه می دهد که در داخل آن از await
استفاده کنیم.
کلیدواژه await
قبل از Promise باعث می شود تا اجرا شدن آن صبر کند و سپس:
۱. اگر خطایی رخ دهد، یک exception به وجود می آید – مانند حالتی که throw error
در آن محل فراخوانی شود.
۲. در غیر این صورت، نتیجه را برمی گرداند.
این دو در کنار هم چهارچوبی عالی برای نوشتن کد های همزمان (asynchronous) فراهم می کنند که هم برای نوشتن و هم برای خواندن راحت است.
با async/await
به ندرت نیاز به نوشتن promise.then/catch
داریم اما نباید فراموش کنیم که آنها بر پایه Promise ها هستند چون برخی اوقات (مانند بیرونی ترین منطقه) ما باید از این متد ها استفاده کنیم. همچنین زمانی که می خواهیم چند کار را به طور همزمان انجام دهیم Promise.all
گزینه مناسبی است.