توابع Async: دادن وعده های دوستانه

توابع Async به شما این امکان را می دهند که کد مبتنی بر قول را طوری بنویسید که گویی همزمان است.

توابع Async به طور پیش‌فرض در کروم، اج، فایرفاکس و سافاری فعال هستند و کاملاً شگفت‌انگیز هستند. آنها به شما این امکان را می دهند که کد مبتنی بر وعده را به گونه ای بنویسید که گویی همزمان است، اما بدون مسدود کردن رشته اصلی. آنها کد ناهمزمان شما را کمتر "هوشمندانه" و خواناتر می کنند.

توابع Async به این صورت عمل می کنند:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

اگر از کلمه کلیدی async قبل از تعریف تابع استفاده می کنید، سپس می توانید از await در تابع استفاده کنید. هنگامی که await یک وعده هستید، عملکرد به صورت غیر مسدود کننده متوقف می شود تا زمانی که وعده حل شود. اگر وعده محقق شود، ارزش را پس می گیرید. اگر قول رد شود، مقدار رد شده پرتاب می شود.

پشتیبانی از مرورگر

پشتیبانی مرورگر

  • کروم: 55.
  • لبه: 15.
  • فایرفاکس: 52.
  • سافاری: 10.1.

منبع

مثال: ثبت واکشی

فرض کنید می خواهید یک URL واکشی کنید و پاسخ را به صورت متن ثبت کنید. در اینجا نحوه استفاده از وعده ها به نظر می رسد:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

و در اینجا همان مورد با استفاده از توابع async وجود دارد:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

این تعداد خطوط یکسان است، اما همه تماس‌ها از بین رفته‌اند. این کار خواندن آن را آسان‌تر می‌کند، مخصوصاً برای کسانی که کمتر با وعده‌ها آشنا هستند.

مقادیر بازگشتی ناهمگام

توابع Async همیشه یک وعده را برمی‌گردانند، چه از await استفاده کنید یا نه. این وعده با هر چیزی که تابع async برمی‌گرداند حل می‌شود، یا با هر چیزی که تابع async پرتاب می‌کند رد می‌شود. بنابراین با:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

… فراخوانی hello() وعده ای را برمی گرداند که با "world" محقق می شود .

async function foo() {
  await wait(500);
  throw Error('bar');
}

… فراخوانی foo() وعده ای را برمی گرداند که با Error('bar') رد می شود .

مثال: جریان یک پاسخ

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

اینجا با وعده هاست:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

جیک "دارنده وعده ها" آرچیبالد، مرا چک کن. ببینید چگونه من در درون خودش processResult() برای راه اندازی یک حلقه ناهمزمان فراخوانی می کنم؟ نوشتن که به من احساس بسیار باهوشی داد. اما مانند بسیاری از کدهای "هوشمند"، شما باید چندین سال به آن خیره شوید تا بفهمید که چه کار می کند، مانند یکی از آن تصاویر چشم جادویی از دهه 90.

بیایید دوباره آن را با توابع async امتحان کنیم:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

تمام "هوشمندی ها" از بین رفته است. حلقه ناهمزمانی که باعث می‌شود من احساس رضایت کنم، با حلقه‌ای قابل اعتماد، کسل‌کننده، جایگزین شده است. خیلی بهتره در آینده، تکرارگرهای همگام را دریافت خواهید کرد، که حلقه while را با یک حلقه for-of جایگزین می‌کند و آن را مرتب‌تر می‌کند.

دیگر نحو تابع ناهمگام

من قبلاً به شما async function() {} نشان داده‌ام، اما کلمه کلیدی async را می‌توان با دستور تابع دیگر استفاده کرد:

توابع پیکان

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

روش های شی

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

روش های کلاس

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

مراقب باشید! از رفتن بیش از حد متوالی خودداری کنید

اگرچه در حال نوشتن کدی هستید که همزمان به نظر می رسد، مطمئن شوید که فرصت انجام کارها را به صورت موازی از دست ندهید.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

تکمیل موارد فوق 1000 میلی ثانیه طول می کشد، در حالی که:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

تکمیل موارد فوق 500 میلی‌ثانیه طول می‌کشد، زیرا هر دو انتظار همزمان اتفاق می‌افتند. بیایید به یک مثال عملی نگاه کنیم.

مثال: خروجی واکشی به ترتیب

فرض کنید می‌خواهید یک سری URL واکشی کنید و در اسرع وقت آنها را به ترتیب صحیح وارد کنید.

نفس عمیق - در اینجا چگونه به نظر می رسد با وعده ها:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

بله، درست است، من reduce برای زنجیره ای از وعده ها استفاده می کنم. من خیلی باهوشم اما این کمی کدنویسی هوشمندانه است که بدون آن بهتر است.

با این حال، هنگام تبدیل موارد فوق به یک تابع همگام، وسوسه انگیز است که بیش از حد متوالی پیش بروید:

توصیه نمی شود - بیش از حد متوالی
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
به نظر بسیار تمیزتر می رسد، اما واکشی دوم من تا زمانی که اولین واکشی من به طور کامل خوانده نشود شروع نمی شود و غیره. این بسیار کندتر از مثال وعده‌هایی است که واکشی‌ها را به صورت موازی انجام می‌دهد. خوشبختانه یک حد وسط ایده آل وجود دارد.
توصیه می شود - زیبا و موازی
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
در این مثال، URL ها به صورت موازی واکشی و خوانده می شوند، اما بیت reduce «هوشمند» با یک حلقه استاندارد، خسته کننده و قابل خواندن جایگزین می شود.

راه حل پشتیبانی مرورگر: ژنراتورها

اگر مرورگرهایی را هدف قرار می‌دهید که از ژنراتورها پشتیبانی می‌کنند (که شامل آخرین نسخه هر مرورگر اصلی می‌شود)، می‌توانید توابع همگام‌سازی polyfill را مرتب کنید.

Babel این کار را برای شما انجام خواهد داد، در اینجا یک مثال از طریق Babel REPL آورده شده است

من رویکرد transpiling را توصیه می‌کنم، زیرا زمانی که مرورگرهای هدف شما از توابع async پشتیبانی می‌کنند، می‌توانید آن را خاموش کنید، اما اگر واقعاً نمی‌خواهید از ترانسپایلر استفاده کنید، می‌توانید از Babel's polyfill استفاده کنید و خودتان از آن استفاده کنید. به جای:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

... شما باید polyfill را اضافه کنید و بنویسید:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

توجه داشته باشید که باید یک ژنراتور ( function* ) را برای createAsyncFunction ارسال کنید و به جای await از yield استفاده کنید. به غیر از این کار یکسان است.

راه حل: احیا کننده

اگر مرورگرهای قدیمی‌تری را هدف قرار می‌دهید، Babel همچنین می‌تواند ژنراتورها را انتقال دهد و به شما امکان می‌دهد تا از عملکردهای همگام‌سازی تا IE8 استفاده کنید. برای انجام این کار به پیش‌تنظیم es2017 بابل و پیش‌تنظیم es2015 نیاز دارید.

خروجی چندان زیبا نیست ، بنابراین مراقب کد-bloat باشید.

همگام سازی همه چیز!

هنگامی که توابع async در همه مرورگرها قرار گرفتند، از آنها در هر تابع وعده بازگشتی استفاده کنید! آنها نه تنها کد شما را مرتب‌تر می‌کنند، بلکه مطمئن می‌شوند که عملکرد همیشه یک وعده را برمی‌گرداند.

من در سال 2014 در مورد توابع async بسیار هیجان زده شدم، و دیدن آنها به صورت واقعی در مرورگرها بسیار عالی است. اوف!