Async/await ، یک Syntax ویژه برای کار با Promise های جاوا اسکریپت،به شیوهای راحتتر است. که درک و استفاده از آن، به طرز شگفتانگیزی آسان است.
توابع async
بیایید با کلمه کلیدی async شروع کنیم. می توان آن را قبل از یک تابع قرار داد، مانند این:
async function f() {
return 1;
}
کلمه “async” قبل از یک تابع به معنای یک چیز ساده است: یک تابع همیشه یک Promise برمیگرداند. سایر مقادیر به طور خودکار در یک Promise حل شده Wrap میشوند.
به عنوان مثال، این تابع یک 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 را برمیگرداند، و Non-promise ها را در آن قرار میدهد. به اندازه کافی ساده بود. نه؟ اما کل ماجرا این نبود. کلمه کلیدی دیگری به نام await وجود دارد،که فقط در توابع async کار میکند، و بسیار جالب است.
await
Syntax:
// works only inside async functions
let value = await promise;
کلمه کلیدی await باعث می شود جاوا اسکریپت منتظر بماند تا این Promise حل شود و نتیجه خود را برگرداند.
در اینجا مثالی با یک Promise آوردهایم که در 1 ثانیه حل میشود:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
f();
اجرای تابع در خط (*) “پاوز” میشود و زمانی که Promise حل شد، از سر گرفته میشود و نتیجه (Result) به نتیجه آن تبدیل میشود. بنابراین در عرض یک ثانیه، با پیام “done!” انجام شدن کد بالا را نشان میدهد.
بیایید تاکید کنیم: await به معنای واقعی کلمه اجرای تابع را تا زمانی که Promise حل شود، به حالت تعلیق در میآورد و سپس آن را با نتیجه Promise از سر میگیرد. این هیچ هزینهای برای منابع CPU ندارد، زیرا موتور جاوا اسکریپت می تواند کارهای دیگری را در این بین انجام دهد: اجرای اسکریپتهای دیگر، هندل کردن ایونتها و غیره.
این فقط یک Syntax زیباتر برای دریافت نتیجه Promise بجای خود Promise در مرحله بعد است. و خواندن و نوشتن آن نیز آسان تر است.
نمی توان از await در توابع معمولی استفاده کرد
اگر بخواهیم از await در یک تابع non-async استفاده کنیم، یک Syntax error رخ خواهد داد:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
اگر فراموش کنیم که async را قبل از یک تابع قرار دهیم، ممکن است این خطا را دریافت کنیم. همانطور که قبلا گفته شد، await فقط در یک تابع async کار می کند.
بیایید مثال showAvatar() را از فصل Promises chaining بگیریم و آن را با استفاده از async/await بازنویسی کنیم:
باید call های .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 در سطح بالا به خوبی کار میکند. ماژولها در مقاله Modules, introduction موجود در سایت جاوااسکریپت پوشش داده شدهاند.
برای مثال:
// we assume this code runs at top level, inside a module
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
console.log(user);
اگر از ماژولها استفاده نمیکنیم، یا مرورگرهای قدیمیتر باید پشتیبانی شوند، یک دستور العمل جهانی وجود دارد: Wrapping در یک تابع async ناشناس.
مثل این:
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await پذیرنده “thenables”است
await مانند soz.then، به ما امکان میدهد از thenable object ها (آنهایی که متد قابل فراخوانی (call) then دارند) استفاده کنیم. ایده این است که یک object شخص ثالث ممکن است یک Promise نباشد، اما با Promise سازگار باشد: اگر .then را پشتیبانی کند، استفاده از آن با awaitکافی است.
در اینجا یک کلاس دموی Thenable وجود دارد. await زیر نمونههای خود را میپذیرد:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resolve with this.num*2 after 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
async function f() {
// waits for 1 second, then result becomes 2
let result = await new Thenable(1);
alert(result);
}
f();
اگر await یک شی غیر promise با .then دریافت کند، متدی را فراخوانی میکند که توابع داخلی را به عنوان آرگومان حل و رد کند (همانطور که برای یک اجرا کننده معمولی Promise انجام میشود). سپس منتظر بمانید تا یکی از آنها فراخوانی شود (در مثال بالا در خط (*) اتفاق می افتد) و سپس با result ادامه دهید.
متدهای کلاس async
برای اعلان یک متد کلاس async، کافی است آن را با async، prepend کنید:
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (this is the same as (result => alert(result)))
معنی یکسان است: تضمین میکند که مقدار بازگشتی یک Promise است و await را فعال می کند.
هندل کردن ارور
اگر یک Promise به طور معمول حل شود، پس از آن await Promise ، نتیجه را برمیگرداند. اما در مورد رد شدن، ارور میدهد (ارور را throw میکند)، درست مثل اینکه یک عبارت throw در آن خط وجود داشته باشد.
این کد را ببینید:
async function f() {
await Promise.reject(new Error("Whoops!"));
}
… مثل این است:
async function f() {
throw new Error("Whoops!");
}
در شرایط واقعی، Promise ممکن است مدتی طول بکشد تا رد شود. در این صورت، یک تاخیر قبل از اینکه await ارور را throw کند، وجود دارد.
میتوانیم آن خطا را با استفاده از try..catch، به همان روشی که throw معمولی انجام میدهیم، دریافت کنیم:
async function f() {
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
در صورت بروز ارور، control به بلوک catch می پرد. ما همچنین می توانیم چندین خط را wrap کنیم:
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
اگر try..catch را نداشته باشیم، Promise ایجاد شده توسط فراخوانی تابع async f() رد میشود. ما میتوانیم .catch را برای مدیریت آن اضافه کنیم:
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
اگر فراموش کنیم .catch را در آنجا اضافه کنیم، یک ارور Promise unhandled دریافت میکنیم (قابل مشاهده در کنسول). ما میتوانیم چنین خطاهایی را با استفاده از یک کنترلکننده رویداد جهانی unhandledredjection، همانطور که در فصل مدیریت خطا با Promise ها توضیح داده شد، کشف کنیم.
async/wait و soz.then/catch
وقتی از async/await استفاده میکنیم، به ندرت به .tn نیاز داریم، زیرا await انتظار ما را کنترل میکند. و می توانیم به جای .catch از یک try..catch معمولی استفاده کنیم. این معمولا (اما نه همیشه) راحت تر است.
اما در سطح بالای کد، وقتی خارج از هر تابع async هستیم، از نظر Syntax نمیتوانیم از await استفاده کنیم، بنابراین اضافه کردن .then/catch برای رسیدن به نتیجه نهایی یا falling-through error ، یک روش عادی است، مانند خط (*) در مثال بالا.
async/await به خوبی با Promise.all کار میکند
هنگامی که باید منتظر چندین Promise باشیم، میتوانیم آنها را در Promise.all wrap کنیم و سپس منتظر بمانیم:
// wait for the array of results
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);
در صورت ارور دادن، طبق معمول، از Promise ناموفق به Promise.all منتشر میشود، و سپس به یک استثنا تبدیل میشود که میتوانیم با استفاده از try..catch در اطراف call کنیم.
خلاصه
کلمه کلیدی async قبل از یک تابع دو اثر دارد:
- باعث میشود که همیشه یک Promise را برگرداند.
- اجازه میدهد تا await در آن استفاده شود.
کلمه کلیدی await قبل از یک Promise باعث می شود جاوا اسکریپت منتظر بماند تا آن Promise حل شود و سپس:
اگر خطا باشد، یک استثنا ایجاد میشود – مثل اینکه throw error در همان مکان call شود.
در غیر این صورت، نتیجه را برمیگرداند.
آنها با هم یک framework عالی برای نوشتن کدهای ناهمزمان ارائه میدهند که خواندن و نوشتن آن آسان است.
با async/await به ندرت نیاز به نوشتن premt.then/catch داریم، اما هنوز نباید فراموش کنیم که آنها بر اساس Promise ها هستند، زیرا گاهی اوقات (مثلاً در بیرونیترین محدوده) باید از این روشها استفاده کنیم. همچنین Promise.all زمانی خوب است که ما به طور همزمان منتظر بسیاری از Taskها هستیم.
Taskها
بازنویسی با استفاده از async/wait
این کد مثال از فصل Promises chaining را با استفاده از async/await به جای .then/catch بازنویسی کنید:
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new Error(response.status);
}
});
}
loadJson('https://javascript.info/no-such-user.json')
.catch(alert); // Error: 404
راه حل
یادداشتهایی در زیر کد آمده است:
async function loadJson(url) { // (1)
let response = await fetch(url); // (2)
if (response.status == 200) {
let json = await response.json(); // (3)
return json;
}
throw new Error(response.status);
}
loadJson('https://javascript.info/no-such-user.json')
.catch(alert); // Error: 404 (4)
یادداشت:
- تابع loadJson async میشود.
- همه .thenهای داخل با await جایگزین میشوند.
میتوانیم بهجای اینکه منتظر آن باشیم، answer.json() را برگردانیم، مانند این:
if (response.status == 200) {
return response.json(); // (3)
}
سپس کد بیرونی باید منتظر باشد تا آن Promise حل شود. این مورد برای ما مهم نیست.
ارور پرتاب شده از loadJson توسط .catch کنترل می شود. ما نمیتوانیم از await loadJson(…) در آنجا استفاده کنیم، زیرا در یک تابع async نیستیم.
بازنویسی “rethrow” با async/wait
در زیر می توانید مثال “rethrow” را بیابید. آن را با استفاده از async/await به جای .then/catch بازنویسی کنید.
و با async/wait که انجام آن آسان می شودT از شر Recursion به نفع یک loop در demoGithubUser خلاص شوید.
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
function loadJson(url) {
return fetch(url)
.then(response => {
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
});
}
// Ask for a user name until github returns a valid user
function demoGithubUser() {
let name = prompt("Enter a name?", "iliakan");
return loadJson(`https://api.github.com/users/${name}`)
.then(user => {
alert(`Full name: ${user.name}.`);
return user;
})
.catch(err => {
if (err instanceof HttpError && err.response.status == 404) {
alert("No such user, please reenter.");
return demoGithubUser();
} else {
throw err;
}
});
}
demoGithubUser();
راه حل
اینجا هیچ ترفندی وجود ندارد. فقط کافی است .catch را با try..catch در demoGithubUser جایگزین کنید و در صورت نیاز async/wait را اضافه کنید:
class HttpError extends Error {
constructor(response) {
super(`${response.status} for ${response.url}`);
this.name = 'HttpError';
this.response = response;
}
}
async function loadJson(url) {
let response = await fetch(url);
if (response.status == 200) {
return response.json();
} else {
throw new HttpError(response);
}
}
// Ask for a user name until github returns a valid user
async function demoGithubUser() {
let user;
while(true) {
let name = prompt("Enter a name?", "iliakan");
try {
user = await loadJson(`https://api.github.com/users/${name}`);
break; // no error, exit loop
} catch(err) {
if (err instanceof HttpError && err.response.status == 404) {
// loop continues after the alert
alert("No such user, please reenter.");
} else {
// unknown error, rethrow
throw err;
}
}
}
alert(`Full name: ${user.name}.`);
return user;
}
demoGithubUser();
Call کردن async از non-async
ما یک تابع “regular” به نام f داریم. چگونه میتوان تابع asyncwait() را Call کرد و از نتیجه آن در f استفاده کرد؟
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));
return 10;
}
function f() {
// ...what should you write here?
// we need to call async wait() and wait to get 10
// remember, we can't use "await"
}
پینوشت: این task از لحاظ تکنیکال بسیار ساده است، اما این سوال برای توسعهدهندگانی که به تازگی وارد مقوله async/wait شدهاند، بسیار رایج است.
راه حل
این مورد برای زمانی است که دانستن نحوه عملکرد داخلی آن مفید است.
فقط تماس async را به عنوان یک Promise در نظر بگیرید و .then را به آن وصل کنید:
async function wait() {
await new Promise(resolve => setTimeout(resolve, 1000));
return 10;
}
function f() {
// shows 10 after 1 second
wait().then(result => alert(result));
}
f();
Leave feedback about this