نکات مثبت، اشتباهات و نحوه استفاده async/await در جاوااسکریپت

13 خرداد 1398, خواندن در 8 دقیقه

async/await که در ES7 معرفی شد، بهبود شگفت‌انگیزی در برنامه‌نویسی ناهمگام با JavaScript است. این نوع تابع، گزینه‌ای برای استفاده از کد همگام برای دسترسی به منابع ناهمگام بدون این که رشته اصلی مسدود شود، فراهم کرد. گرچه، استفاده درست از آن کمی پیچیده است. در این مقاله، async/await را از زوایای مختلف بررسی خواهیم کرد، و به شما نشان خواهیم داد که چگونه از آن‌ها به صورت صحیح و موثر استفاده کنید.

نکات مثبت درباره async/await

مهم‌ترین منفعتی که async/await برای ما فراهم کرد، استایل برنامه‌نویسی همگام است. بیایید مثالی را ببینیم:

// async/await
async getBooksByAuthorWithAwait(authorId) {
  const books = await bookModel.fetchAll();
  return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

کاملا واضح است که درک نسخه async/await، بسیار آسان‌تر از درک نسخه Promise است. اگر کلمه کلیدی await را نادیده بگیرید، کد مورد نظر درست به مانند هر زبان همگامی مثل پایتون به نظر می‌آید.

و نکته جذاب، خوانا بودن نیست. async/await از مرورگرهای بومی نیز پشتیبانی می‌کند. امروزه، تمام مرورگرهای رایج، کاملا از توابع async پشتیبانی می‌کنند.

پشتیبانی مرورگرهای بومی، یعنی این که مجبور نیستید کد را Transpile کنید. به علاوه، خطایابی نیز ساده‌تر می‌شود. وقتی که در نقطه ورود تابع یک نقطه شکست تعیین می‌کنید و از خط await می‌گذرید، می‌بینید که در حالیکه bookModel.fetchAll() در حال انجام کار خود است، خطایاب برای مدت کوتاهی مکث می‌کند، که البته باید یک نقطه شکست دیگر نیز بر روی خط .filter قرار دهید.

منفعت دیگری که کمتر دیده می‌شود، کلمه کلیدی async است. این کلمه کلیدی، اعلام می‌کند که مقدار برگشتی تابع getBookByAuthorWithAwait() توسط یک Promise تضمین شده است، پس آن فراخوان‌ها می‌توانند به صورت امن، عملیات‌های getBookByAuthorWithAwait().then(…) یا getBookByAuthorWithAwait() را اجرا کنند. این مورد را فرض کنید:

getBooksByAuthorWithPromise(authorId) {
  if (!authorId) {
    return null;
  }
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

در کد بالا، getBookByAuthorWithPromise ممکن است یک Promise (در حالت معمولی) یا مقدار null (در حالات خاص) را برگرداند، که در هیچ صورتی فراخوان مورد نظر نمی‌تواند .then() را به طور امن فراخوانی کند. با استفاده از async، چنین کدی غیر ممکن می‌شود.

Async/await میتواند گمراه کننده باشد

برخی مقالات async/await را با Promise مقایسه می‌کنند و ادعا می‌کنند که async/await نسل بعدی در تکامل برنامه‌نویسی ناهمگام JavaScript است، که من با احترام مخالفم. Async/await یک بهبود است، اما چیزی بیش از یک سینتکس بهتر نیست و استایل برنامه‌نویسی را به کلی تغییر نخواهد داد.

اساسا، توابع async هنوز هم همان Promiseها هستند. قبل از این که بتوانید از توابع async به طور صحیح استفاده کنید، باید Promiseها را درک کنید؛ و حتی بدتر از آن این است که در برخی موارد باید از Promiseها در کنار توابع async استفاده کنید.

توابع getBookByAuthorWithAwait() و getBookByAuthorWithPromises() در مثال بالا را در نظر بگیرید. دقت کنید که آن‌ها نه تنها به طور یکسان کار می‌کنند، بلکه دقیقا رابط مشابهی دارند.

این به این معنی است که اگر getBookByAuthorWithAwait() را به طور مستقیم فراخوانی کنید، یک Promise را بر می‌گرداند.

این لزوما چیز بدی نیست. عبارت «await» این حس را به مردم می‌دهد که این تابع می‌تواند توابع ناهمگام را به توابع همگام تبدیل کند، که این تفکر کاملا غلط است.

اشتباهات async/await

حال در هنگام استفاده از async/await چه اشتباهاتی ممکن است انجام شوند؟ در اینجا برخی اشتباهات رایج را می‌بینید:

کدنویسی بیش از حد متوالی

گرچه await می‌تواند باعث شود کد شما همگام به نظر برسد، در نظر داشته باشید که آن‌ها همچنان ناهمگام هستند و باید در جهت جلوگیری از بیش از حد متوالی شدن آن‌ها، کمی دقت به خرج دهید.

async getBooksAndAuthor(authorId) {
  const books = await bookModel.fetchAll();
  const author = await authorModel.fetch(authorId);
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

این کد از نظر منطقی صحیح است؛ گرچه،‌ در اصل اشتباه است.

  1. Await bookModel.fetchAll() تا زمانی که fetchAll برگردانده شود، منتظر می‌ماند.
  2. تابع await authorModel.fetch(authorID) فرا خوانده می‌شود.

دقت کنید که authorModel.fetch(authorID) به نتیجه bookModel.fetchAll() بستگی ندارد و در واقع آن‌ها می‌توانند در موازات هم فراخوانده شوند. گرچه در اینجا با استفاده از await، این دو فراخوانی متوالی می‌شوند و زمان اجرای کلی بسیار بیشتر از حالت موازی خواهد شد.

نحوه صحیح آن،‌ به این صورت است:

async getBooksAndAuthor(authorId) {
  const bookPromise = bookModel.fetchAll();
  const authorPromise = authorModel.fetch(authorId);
  const book = await bookPromise;
  const author = await authorPromise;
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

یا حتی بدتر، اگر می‌خواهید لیستی از آیتم‌ها را یک به یک بگیرید، باید به یک Promise تکیه کنید:

async getAuthors(authorIds) {
  // روش غلط، این روش باعث بروز فراخوانی‌های متوالی می‌شود
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));

// صحیح
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}

به طور خلاصه، هنوز باید به جریانات کاری به صورت ناهمگام فکر کنید، و سپس سعی کنید با استفاده از await، کد خود را به صورت همگام بنویسید. در جریانات کاری پیچیده‌تر،‌ استفاده از Promise می‌تواند ساده‌تر باشد.

مدیرت خطا

با استفاده از Promiseها، یک تابع async می‌تواند دو مقدار برگشتی داشته باشد: مقدار مصمم، و مقدار رد شده. می‌توانیم از .then() برای حالات معمولی و از .catch() برای حالات خاص استفاده کنیم. گرچه مدیریت خطا با async/await می‌تواند پیچیده باشد.

try…catch

استانداردترین روش (و روش پیشنهادی من) استفاده از بیانیه try…catch است. وقتی که یک فراخونی را با استفاده از توابع await انجام می‌دهیم، هر مقدار رد شده‌ای به عنوان یک Exception در نظر گرفته می‌شود. در زیر، یک مثال در این مورد را می‌بینید:

class BookModel {
  fetchAll() {
    return new Promise((resolve, reject) => {
      window.setTimeout(() => { reject({'error': 400}) }, 1000);
    });
  }
}

// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
  const books = await bookModel.fetchAll();
} catch (error) {
  console.log(error);    // { "error": 400 }
}

خطای catch شده، دقیقا مقدار رد شده است. پس از این که یک Exception را دریافت کردیم، چندین روش برای رسیدگی به آن داریم:

  • Exception را اصلاح کنیم و یک مقدار معمولی را برگردانیم. ( استفاده نکردن از بیانیه return در بلوک catch، معادل با استفاده از return undefined بوده، و همچنین یک مقدار معمولی است)
  • اگر می‌خواهید که فراخوان به آن رسیدگی کند، باید آن Exception را در اصطلاح Throw کنید. هم می‌توانید این کار را به صورت مستقیم و مانند throw error; انجام دهید، که به شما اجازه می‌دهد تا از تابع async getBookByAuthorWithAwait() در یک زنجیره Promise استفاده کنید، و هم می‌توانید با استفاده از آبجکت Error، آن خطا را به صورت throw new Error(error) جمع کنید، که در زمانی که این خطا در کنسول نمایش داده می‌شود، Stack Trace کامل را نشان می‌دهد.
  • آن را به صورت return Promise.reject(error) رد کنید. این روش معادل throw error است و پیشنهاد نمی‌شود.

برتری‌های استفاده از try…catch موارد زیر هستند:

  • ساده و سنتی است. تا زمانی که در زبان‌های دیگر مانند Java یا C++ تجربه داشته باشید، هیچ مشکلی در درک آن نخواهید داشت.
  • اگر مدیریت قدم به قدم خطا لازم نیست، همچنان می‌توانید چنیدن فراخوانی await را در یک بلوک try…catch برای مدیریت خطاها در یک مکان داشته باشید.

همچنین یک اشکال در این روش وجود دارد. از آنجایی که try…catch هر Exceptionای در بلوک را خواهد گرفت، برخی Exceptionهای دیگر که معمولا توسط Promiseها گرفته نمی‌شوند، گرفته خواهند شد. به این مثال فکر کنید:

class BookModel {
  fetchAll() {
    cb();    // note `cb` is undefined and will result an exception
    return fetch('/books');
  }
}

try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // This will print "cb is not defined"
}

این کد را اجرا کنید و ببینید که خطای ReferenceError: cb is not defined را در کنسول و به رنگ سیاه می‌بینید. این خطا توسط console.log() خارج شده بود، اما نه توسط خود JavaScript. گاهی اوقات این می‌تواند وخیم باشد:  اگر BookModel در مجموعه‌ای از فراخوانی توابع، عمیقا بسته شده باشد و یک فراخوانی خطا را دریافت کند، آن وقت تشخیص خطایی مثل این، بسیار سخت خواهد بود.

مجبور کردن توابع به برگرداندن هر دو مقدار

یکی دیگر از روش‌های مدیریت خطا، از زبان Go الهام گرفته شده است. این روش به توابع async اجازه می‌دهد که هم خود خطا، و هم نتیجه را برگردانند.

به طور خلاصه، می‌توانید از توابع async به این صورت استفاده کنید:

[err, user] = await to(UserModel.findById(1));

به شخصه از آنجایی که این روش استایل Go را به JavaScript می‌آورد، آن را نمی‌پسندم، اما در برخی موارد می‌تواند خوب باشد.

استفاده از .catch

روش پایانی که در اینجا معرفی می‌کنیم، استفاده از .catch() است.

نحوه عملکرد await را به یاد بیاورید: این تابع صبر می‌کند تا یک Promise کار خود را انجام دهد. همچنین به یاد بیاورید که promise.catch() هم یک Promise را بر می‌گرداند. پس می‌توانیم مدیریت خطا را به این صورت بنویسیم:

// books === خطای تعریف نشده بروز می‌دهد
// از آنجایی که در بیانیه تعریف شده چیزی برگردانده نشده است
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });

یک اشکال جزئی در این روش وجود دارند:

این روش، ترکیبی از Promiseها و توابع async است. همچنان باید درک کنید که Promiseها چگونه کار می‌کنند.

نتیجه گیری

کلمه کلیدی async/await که در ES7 معرفی شد، قطعا بهبودی به برنامه‌نویسی ناهمگام JavaScript است. این نوع توابع باعث می‌شوند که خواندن و خطایابی کد آسان‌تر شود. گرچه اگر شخصی می‌خواهد به درستی از آن‌ها استفاده کند، باید Promiseها را به خوبی درک کند، و تکنیک اصلی مورد استفاده، هنوز هم Promiseها است.

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
عرفان کاکایی @er79ka
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو