Нека да разгледаме Async/Await в JavaScript, използвайки примери

Авторът на статията разглежда примери за Async/Await в JavaScript. Като цяло Async/Await е удобен начин за писане на асинхронен код. Преди да се появи тази функция, такъв код беше написан с помощта на обратни извиквания и обещания. Авторът на оригиналната статия разкрива предимствата на Async/Await, като анализира различни примери.

Напомняме ви: за всички читатели на "Habr" - отстъпка от 10 000 рубли при записване във всеки курс Skillbox, използвайки промоционалния код на "Habr".

Skillbox препоръчва: Образователен онлайн курс "Разработчик на Java".

ОбрПов

Обратното извикване е функция, чието извикване се забавя за неопределено време. Преди това обратното извикване се използваше в онези области на кода, където резултатът не можеше да бъде получен веднага.

Ето пример за асинхронно четене на файл в Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Проблеми възникват, когато трябва да извършите няколко асинхронни операции наведнъж. Нека си представим този сценарий: направена е заявка към потребителската база данни на Arfat, трябва да прочетете нейното поле profile_img_url и да изтеглите изображение от сървъра someserver.com.
След изтеглянето конвертираме изображението в друг формат, например от PNG в JPEG. Ако преобразуването е било успешно, на имейла на потребителя се изпраща писмо. След това във файла transformations.log се въвежда информация за събитието, като се посочва датата.

Струва си да се обърне внимание на припокриването на обратните извиквания и големия брой }) в последната част на кода. Нарича се Callback Hell или Pyramid of Doom.

Недостатъците на този метод са очевидни:

  • Този код е труден за четене.
  • Също така е трудно да се справят с грешки, което често води до лошо качество на кода.

За да се реши този проблем, към JavaScript бяха добавени обещания. Те ви позволяват да замените дълбокото влагане на обратни извиквания с думата .then.

Положителният аспект на обещанията е, че те правят кода много по-четлив, отгоре надолу, а не отляво надясно. Обещанията обаче също имат своите проблеми:

  • Трябва да добавите много .then.
  • Вместо try/catch, .catch се използва за обработка на всички грешки.
  • Работата с множество обещания в рамките на един цикъл не винаги е удобна; в някои случаи те усложняват кода.

Ето една задача, която ще покаже значението на последната точка.

Да предположим, че имаме for цикъл, който отпечатва поредица от числа от 0 до 10 на произволни интервали (0–n секунди). Използвайки обещания, трябва да промените този цикъл, така че числата да се отпечатват в последователност от 0 до 10. Така че, ако са необходими 6 секунди за отпечатване на нула и 2 секунди за отпечатване на единица, първо трябва да се отпечата нулата и след това ще започне обратното броене за отпечатване на единия.

И разбира се, ние не използваме Async/Await или .sort за решаване на този проблем. Примерно решение е в края.

Асинхронни функции

Добавянето на асинхронни функции в ES2017 (ES8) опрости задачата за работа с обещания. Отбелязвам, че асинхронните функции работят „върху“ обещанията. Тези функции не представляват качествено различни концепции. Асинхронните функции са предназначени като алтернатива на кода, който използва обещания.

Async/Await дава възможност да се организира работата с асинхронен код в синхронен стил.

По този начин познаването на обещанията улеснява разбирането на принципите на Async/Await.

синтаксис

Обикновено се състои от две ключови думи: async и await. Първата дума превръща функцията в асинхронна. Такива функции позволяват използването на await. Във всеки друг случай използването на тази функция ще генерира грешка.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async се вмъква в самото начало на декларацията на функцията, а в случай на функция със стрелка, между знака „=“ и скобите.

Тези функции могат да бъдат поставени в обект като методи или използвани в декларация на клас.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

NB! Струва си да запомните, че конструкторите на класове и гетерите/сетерите не могат да бъдат асинхронни.

Семантика и правила за изпълнение

Async функциите са основно подобни на стандартните JS функции, но има изключения.

По този начин асинхронните функции винаги връщат обещания:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

По-конкретно, fn връща низа hello. Е, тъй като това е асинхронна функция, стойността на низа е обвита в обещание с помощта на конструктор.

Ето алтернативен дизайн без Async:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

В този случай обещанието се връща „ръчно“. Асинхронна функция винаги е обвита в ново обещание.

Ако върнатата стойност е примитив, async функцията връща стойността, като я обгръща в обещание. Ако върнатата стойност е обект на обещание, неговата резолюция се връща в ново обещание.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

Но какво се случва, ако има грешка вътре в асинхронна функция?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Ако не се обработи, foo() ще върне обещание с отхвърляне. В тази ситуация ще бъде върнат Promise.reject, съдържащ грешка, вместо Promise.resolve.

Асинхронните функции винаги извеждат обещание, независимо какво се връща.

Асинхронните функции спират на пауза при всяко изчакване.

Изчакването засяга изразите. Така че, ако изразът е обещание, асинхронната функция се спира, докато обещанието не бъде изпълнено. Ако изразът не е обещание, той се преобразува в обещание чрез Promise.resolve и след това се завършва.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

А ето и описание как работи функцията fn.

  • След извикването му, първият ред се преобразува от const a = await 9; in const a = await Promise.resolve(9);.
  • След използване на Await, изпълнението на функцията се спира, докато a получи своята стойност (в текущата ситуация е 9).
  • delayAndGetRandom(1000) спира изпълнението на функцията fn, докато не завърши сама (след 1 секунда). Това ефективно спира функцията fn за 1 секунда.
  • delayAndGetRandom(1000) чрез resolve връща произволна стойност, която след това се присвоява на променлива b.
  • Е, случаят с променлива c е подобен на случая с променлива a. След това всичко спира за секунда, но сега delayAndGetRandom(1000) не връща нищо, защото не е задължително.
  • В резултат на това стойностите се изчисляват по формулата a + b * c. Резултатът се обвива в обещание с помощта на Promise.resolve и се връща от функцията.

Тези паузи може да напомнят на генераторите в ES6, но има нещо в това вашите причини.

разрешаване на проблема

Е, сега нека разгледаме решението на проблема, споменат по-горе.

Функцията finishMyTask използва Await, за да изчака резултатите от операции като queryDatabase, sendEmail, logTaskInFile и други. Ако сравните това решение с това, при което бяха използвани обещания, приликите ще станат очевидни. Версията Async/Await обаче значително опростява всички синтактични сложности. В този случай няма голям брой обратни извиквания и вериги като .then/.catch.

Ето решение с извеждане на числа, има два варианта.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

И ето решение, използващо асинхронни функции.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Грешка при работа

Необработените грешки са обвити в отхвърлено обещание. Асинхронните функции обаче могат да използват try/catch за синхронно обработване на грешки.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() е асинхронна функция, която или успява („перфектно число“) или се проваля с грешка („Съжалявам, числото е твърде голямо“).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Тъй като примерът по-горе очаква canRejectOrReturn да се изпълни, неговият собствен отказ ще доведе до изпълнението на catch блока. В резултат на това функцията foo ще завърши или с undefined (когато нищо не се връща в блока try) или с уловена грешка. В резултат на това тази функция няма да се провали, защото try/catch ще се справи със самата функция foo.

Ето още един пример:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Струва си да се обърне внимание на факта, че в примера canRejectOrReturn се връща от foo. Foo в този случай или завършва с перфектно число, или връща грешка („Съжалявам, числото е твърде голямо“). Блокът catch никога няма да бъде изпълнен.

Проблемът е, че foo връща обещанието, предадено от canRejectOrReturn. Така че решението на foo става решение на canRejectOrReturn. В този случай кодът ще се състои само от два реда:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Ето какво се случва, ако използвате изчакване и връщане заедно:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

В кода по-горе foo ще излезе успешно както с перфектно число, така и с уловена грешка. Тук няма да има откази. Но foo ще върне с canRejectOrReturn, а не с undefined. Нека се уверим в това, като премахнем реда за връщане, чакащ canRejectOrReturn():

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Често срещани грешки и клопки

В някои случаи използването на Async/Await може да доведе до грешки.

Забравено чакане

Това се случва доста често - ключовата дума await е забравена преди обещанието:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

Както можете да видите, в кода няма чакане или връщане. Следователно foo винаги излиза с undefined без забавяне от 1 секунда. Но обещанието ще бъде изпълнено. Ако изведе грешка или отхвърляне, ще бъде извикано предупреждение за UnhandledPromiseRejectionWarning.

Асинхронни функции в обратните извиквания

Асинхронните функции доста често се използват в .map или .filter като обратни извиквания. Пример е функцията fetchPublicReposCount(username), която връща броя на отворените хранилища в GitHub. Да кажем, че има трима потребители, чиито показатели ни трябват. Ето кода за тази задача:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Имаме нужда от акаунти на ArfatSalman, octocat, norvig. В този случай ние правим:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Струва си да обърнете внимание на Await в обратното извикване на .map. Тук counts е масив от обещания, а .map е анонимно обратно извикване за всеки определен потребител.

Прекалено последователно използване на await

Нека вземем този код като пример:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

Тук репо номерът се поставя в променливата count, след което този номер се добавя към масива counts. Проблемът с кода е, че докато данните за първия потребител не пристигнат от сървъра, всички следващи потребители ще бъдат в режим на готовност. По този начин само един потребител се обработва наведнъж.

Ако например са необходими около 300 ms за обработка на един потребител, тогава за всички потребители това вече е секунда; изразходваното време зависи линейно от броя на потребителите. Но тъй като получаването на броя на репо не зависи едно от друго, процесите могат да бъдат паралелизирани. Това изисква работа с .map и Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all получава масив от обещания като вход и връща обещание. Последният, след като всички обещания в масива са изпълнени или при първото отхвърляне, е завършен. Може да се случи всички те да не стартират по едно и също време - за да осигурите едновременно стартиране, можете да използвате p-map.

Заключение

Асинхронните функции стават все по-важни за разработката. Е, за адаптивно използване на асинхронни функции трябва да използвате Асинхронни итератори. Разработчикът на JavaScript трябва да е добре запознат с това.

Skillbox препоръчва:

Източник: www.habr.com

Добавяне на нов коментар