Låt oss titta på Async/Await i JavaScript med hjälp av exempel

Författaren till artikeln undersöker exempel på Async/Await i JavaScript. Sammantaget är Async/Await ett bekvämt sätt att skriva asynkron kod. Innan den här funktionen dök upp skrevs sådan kod med hjälp av återuppringningar och löften. Författaren till den ursprungliga artikeln avslöjar fördelarna med Async/Await genom att analysera olika exempel.

Påminnelse: för alla läsare av "Habr" - en rabatt på 10 000 rubel när du anmäler dig till någon Skillbox-kurs med hjälp av "Habr"-kampanjkoden.

Skillbox rekommenderar: Pedagogisk onlinekurs "Java-utvecklare".

Återuppringning

Återuppringning är en funktion vars samtal är försenat på obestämd tid. Tidigare användes callbacks i de kodområden där resultatet inte kunde erhållas omedelbart.

Här är ett exempel på asynkron läsning av en fil i Node.js:

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

Problem uppstår när du behöver utföra flera asynkrona operationer samtidigt. Låt oss föreställa oss det här scenariot: en begäran görs till Arfat-användardatabasen, du måste läsa dess profile_img_url-fält och ladda ner en bild från someserver.com-servern.
Efter nedladdning konverterar vi bilden till ett annat format, till exempel från PNG till JPEG. Om konverteringen lyckades skickas ett brev till användarens e-post. Därefter läggs information om händelsen in i filen transformations.log, som anger datumet.

Det är värt att uppmärksamma överlappningen av återuppringningar och det stora antalet }) i den sista delen av koden. Det kallas Callback Hell eller Pyramid of Doom.

Nackdelarna med denna metod är uppenbara:

  • Denna kod är svår att läsa.
  • Det är också svårt att hantera fel, vilket ofta leder till dålig kodkvalitet.

För att lösa detta problem lades löften till JavaScript. De låter dig ersätta djup kapsling av återuppringningar med ordet .then.

Den positiva aspekten med löften är att de gör koden mycket bättre läsbar, uppifrån och ner snarare än från vänster till höger. Men löften har också sina problem:

  • Du måste lägga till mycket .då.
  • Istället för try/catch, används .catch för att hantera alla fel.
  • Att arbeta med flera löften inom en slinga är inte alltid bekvämt, i vissa fall komplicerar de koden.

Här är ett problem som visar innebörden av den sista punkten.

Anta att vi har en for-loop som skriver ut en talföljd från 0 till 10 med slumpmässiga intervall (0–n sekunder). Med hjälp av löften måste du ändra denna loop så att siffrorna skrivs ut i sekvens från 0 till 10. Så om det tar 6 sekunder att skriva ut en nolla och 2 sekunder att skriva ut en etta, ska nollan skrivas ut först och sedan nedräkningen för att skriva ut den börjar.

Och naturligtvis använder vi inte Async/Await eller .sort för att lösa detta problem. Ett exempel på lösning finns i slutet.

Asynkrona funktioner

Tillägget av asynkrona funktioner i ES2017 (ES8) förenklade uppgiften att arbeta med löften. Jag noterar att asynkrona funktioner fungerar "utöver löften". Dessa funktioner representerar inte kvalitativt olika begrepp. Async-funktioner är tänkta som ett alternativ till kod som använder löften.

Async/Await gör det möjligt att organisera arbetet med asynkron kod i en synkron stil.

Att känna till löften gör det alltså lättare att förstå principerna för Async/Await.

syntax

Normalt består den av två nyckelord: asynkron och vänta. Det första ordet gör funktionen till asynkron. Sådana funktioner tillåter användning av vänta. I alla andra fall kommer användning av denna funktion att generera ett fel.

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

Async infogas i början av funktionsdeklarationen, och i fallet med en pilfunktion, mellan "="-tecknet och parenteserna.

Dessa funktioner kan placeras i ett objekt som metoder eller användas i en klassdeklaration.

// 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');
  }
}

OBS! Det är värt att komma ihåg att klasskonstruktörer och getters/setters inte kan vara asynkrona.

Semantik och utföranderegler

Asynkrona funktioner liknar i grunden vanliga JS-funktioner, men det finns undantag.

Således returnerar asynkronfunktioner alltid löften:

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

Närmare bestämt returnerar fn strängen hej. Tja, eftersom detta är en asynkron funktion, är strängvärdet insvept i ett löfte med hjälp av en konstruktor.

Här är en alternativ design utan Async:

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

I detta fall returneras löftet "manuellt". En asynkron funktion är alltid insvept i ett nytt löfte.

Om returvärdet är ett primitivt, returnerar asynkronfunktionen värdet genom att slå in det i ett löfte. Om returvärdet är ett löftesobjekt returneras dess upplösning i ett nytt löfte.

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

Men vad händer om det finns ett fel i en asynkron funktion?

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

Om det inte behandlas kommer foo() att returnera ett löfte med avslag. I den här situationen kommer Promise.reject som innehåller ett fel att returneras istället för Promise.resolve.

Asynkrona funktioner ger alltid ett löfte, oavsett vad som returneras.

Asynkrona funktioner pausar vid varje väntan.

Avvakta påverkar uttryck. Så, om uttrycket är ett löfte, avbryts asynkronfunktionen tills löftet är uppfyllt. Om uttrycket inte är ett löfte omvandlas det till ett löfte via Promise.resolve och fullföljs sedan.

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

Och här är en beskrivning av hur fn-funktionen fungerar.

  • Efter att ha anropat den, konverteras den första raden från const a = await 9; in const a = await Promise.resolve(9);.
  • Efter att ha använt Await avbryts funktionsexekveringen tills a får sitt värde (i nuvarande situation är det 9).
  • delayAndGetRandom(1000) pausar exekveringen av fn-funktionen tills den slutför sig själv (efter 1 sekund). Detta stoppar effektivt fn-funktionen i 1 sekund.
  • delayAndGetRandom(1000) via resolve returnerar ett slumpmässigt värde, som sedan tilldelas variabeln b.
  • Jo, fallet med variabel c liknar fallet med variabel a. Efter det stannar allt för en sekund, men nu returnerar delayAndGetRandom(1000) ingenting eftersom det inte krävs.
  • Som ett resultat beräknas värdena med formeln a + b * c. Resultatet är insvept i ett löfte med hjälp av Promise.resolve och returneras av funktionen.

Dessa pauser kan påminna om generatorer i ES6, men det är något med det dina skäl.

Löser problemet

Nåväl, låt oss nu titta på lösningen på problemet som nämns ovan.

Funktionen finishMyTask använder Await för att vänta på resultatet av operationer som queryDatabase, sendEmail, logTaskInFile och andra. Om man jämför denna lösning med den där löften användes, blir likheterna uppenbara. Async/Await-versionen förenklar dock avsevärt alla syntaktiska komplexiteter. I det här fallet finns det inget stort antal callbacks och kedjor som .then/.catch.

Här är en lösning med utdata av siffror, det finns två alternativ.

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

Och här är en lösning som använder asynkrona funktioner.

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

Fel vid bearbetning

Ohanterade fel lindas in i ett avvisat löfte. Asynkrona funktioner kan dock använda försök/fånga för att hantera fel synkront.

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() är en asynkron funktion som antingen lyckas ("perfekt antal") eller misslyckas med ett fel ("Tyvärr, numret för stort").

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

Eftersom exemplet ovan förväntar sig att canRejectOrReturn körs, kommer dess eget misslyckande att resultera i exekvering av catch-blocket. Som ett resultat kommer funktionen foo att sluta med antingen odefinierad (när inget returneras i försöksblocket) eller med ett fel fångat. Som ett resultat kommer denna funktion inte att misslyckas eftersom try/catch kommer att hantera funktionen foo själv.

Här är ett annat exempel:

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

Det är värt att uppmärksamma det faktum att i exemplet returneras canRejectOrReturn från foo. Foo i detta fall avslutas antingen med ett perfekt nummer eller returnerar ett fel ("Tyvärr, nummer för stort"). Fångstblocket kommer aldrig att exekveras.

Problemet är att foo returnerar löftet från canRejectOrReturn. Så lösningen på foo blir lösningen på canRejectOrReturn. I det här fallet kommer koden endast att bestå av två rader:

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

Här är vad som händer om ni använder await och returnerar tillsammans:

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

I koden ovan kommer foo att avslutas framgångsrikt med både ett perfekt nummer och ett fel fångat. Det blir inga avslag här. Men foo kommer tillbaka med canRejectOrReturn, inte med undefined. Låt oss se till detta genom att ta bort return await canRejectOrReturn()-raden:

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

Vanliga misstag och fallgropar

I vissa fall kan användning av Async/Await leda till fel.

Glömt väntar

Detta händer ganska ofta - nyckelordet vänta glöms bort innan löftet:

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

Som du kan se finns det ingen väntan eller retur i koden. Därför går foo alltid ut med odefinierat utan 1 sekunds fördröjning. Men löftet kommer att uppfyllas. Om det ger ett fel eller avslag kommer UnhandledPromiseRejectionWarning att anropas.

Asynkrona funktioner i återuppringningar

Async-funktioner används ganska ofta i .map eller .filter som återuppringningar. Ett exempel är funktionen fetchPublicReposCount(användarnamn), som returnerar antalet öppna arkiv på GitHub. Låt oss säga att det finns tre användare vars mätvärden vi behöver. Här är koden för denna uppgift:

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'];
}

Vi behöver ArfatSalman, octocat, norvig konton. I det här fallet gör vi:

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

Det är värt att uppmärksamma Await i .map-återuppringningen. Här räknas är en rad löften, och .map är en anonym återuppringning för varje angiven användare.

Alltför konsekvent användning av vänta

Låt oss ta den här koden som ett exempel:

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;
}

Här placeras repo-numret i count-variabeln, sedan läggs detta nummer till counts-matrisen. Problemet med koden är att tills den första användarens data kommer från servern kommer alla efterföljande användare att vara i standby-läge. Således bearbetas endast en användare åt gången.

Om det till exempel tar cirka 300 ms att bearbeta en användare, så är det för alla användare redan en sekund, tiden som spenderas linjärt beror på antalet användare. Men eftersom erhållandet av antalet repor inte är beroende av varandra, kan processerna parallelliseras. Detta kräver att du arbetar med .map och 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 får en mängd löften som input och returnerar ett löfte. Det senare, efter att alla löften i arrayen har fullbordats eller vid det första avslaget, fullbordas. Det kan hända att de alla inte startar samtidigt - för att säkerställa samtidig start kan du använda p-map.

Slutsats

Asynkrona funktioner blir allt viktigare för utvecklingen. Tja, för adaptiv användning av asynkrona funktioner bör du använda Asynkrona iteratorer. En JavaScript-utvecklare bör vara väl insatt i detta.

Skillbox rekommenderar:

Källa: will.com

Lägg en kommentar