3 مهر 1402
تهران، خیابان آزادی، تقاطع قریب
برنامه نویسی نرم افزار

آموزش async / await در JavaScript

آموزش Async / await در JavaScript

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();
				
			

منبع ترجمه: javascript.info

Leave feedback about this

  • کیفیت
  • قیمت
  • خدمات

PROS

+
Add Field

CONS

+
Add Field
Choose Image
Choose Video
X