Verstaan ​​Async/Wag in JavaScript met voorbeelde

Die skrywer van die artikel ontleed Async / Await in JavaScript met voorbeelde. Oor die algemeen is Async/Wag 'n gerieflike manier om asinchrone kode te skryf. Voordat hierdie kenmerk verskyn het, is so 'n kode geskryf deur terugbelle en beloftes te gebruik. Die skrywer van die oorspronklike artikel breek die voordele van Async/Await op deur na verskeie voorbeelde te kyk.

Ons herinner: vir alle lesers van "Habr" - 'n afslag van 10 000 roebels wanneer u inskryf vir enige Skillbox-kursus met behulp van die "Habr"-promosiekode.

Skillbox beveel aan: Opvoedkundige aanlyn kursus "Java-ontwikkelaar".

Terugbel

Terugbel is 'n funksie waarvan die oproep onbepaald vertraag word. Voorheen is terugbelopings gebruik in daardie dele van die kode waar die resultaat nie dadelik verkry kon word nie.

Hier is 'n voorbeeld van asynchrone lees van 'n lêer in Node.js:

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

Probleme ontstaan ​​wanneer jy verskeie asynchrone bewerkings gelyktydig moet uitvoer. Kom ons stel ons die volgende scenario voor: 'n versoek word aan die databasis van die Arfat-gebruiker gerig, jy moet sy profile_img_url-veld lees en die prent van die someserver.com-bediener aflaai.
Nadat ons dit opgelaai het, skakel ons die prent om na 'n ander formaat, byvoorbeeld van PNG na JPEG. As die omskakeling suksesvol was, word 'n brief na die gebruiker se pos gestuur. Verdere inligting oor die gebeurtenis word in die transformations.log-lêer met die datum ingevoer.

Dit is die moeite werd om aandag te gee aan die oorvleuelende terugbele en die groot aantal }) in die laaste deel van die kode. Dit word Callback Hell of Pyramid of Doom genoem.

Die nadele van hierdie metode is duidelik:

  • Hierdie kode is moeilik om te lees.
  • Dit is ook moeilik om foute te hanteer, wat dikwels lei tot swak kodegehalte.

Beloftes is by JavaScript gevoeg om hierdie probleem op te los. Hulle laat jou toe om die diep nesting van terugbelopings te vervang met die woord .dan.

Die goeie ding van beloftes is dat dit kode baie makliker maak om te lees, van bo na onder in plaas van links na regs. Beloftes het egter ook hul eie probleme:

  • Jy moet baie .dan byvoeg.
  • In plaas van try/catch word .catch gebruik om alle foute te hanteer.
  • Om met verskeie beloftes binne een siklus te werk, is ver van altyd gerieflik, in sommige gevalle bemoeilik dit die kode.

Hier is 'n taak wat die waarde van die laaste item sal wys.

Gestel ons het 'n for-lus wat 'n reeks getalle van 0 tot 10 met 'n ewekansige interval (0-n sekondes) uitvoer. Deur beloftes te gebruik, moet jy hierdie lus verander sodat die nommers in volgorde van 0 tot 10 vertoon word. Dus, as die uitset van nul 6 sekondes neem, en die eenhede - 2 sekondes, moet nul eers uitgevoer word, en dan die telling van die uitset van een sal begin.

En natuurlik gebruik ons ​​nie Async/Await of .sort om hierdie probleem op te los nie. 'n Voorbeeld oplossing is aan die einde.

Async-funksies

Die byvoeging van asynchrone funksies in ES2017 (ES8) het die taak om met beloftes te werk makliker gemaak. Ek let daarop dat asinkroniseringfunksies "bo-op" beloftes werk. Hierdie funksies verteenwoordig nie kwalitatief verskillende konsepte nie. Async-funksies is ontwerp as 'n alternatief vir kode wat beloftes gebruik.

Async/Await maak dit moontlik om werk met asinchroniese kode in 'n sinchroniese styl te organiseer.

So, kennis van beloftes maak dit makliker om die beginsels van Async/Wag te verstaan.

sintaksis

In 'n normale situasie bestaan ​​dit uit twee sleutelwoorde: asinc en wag. Die eerste woord maak die funksie asynchronies. Sulke funksies laat die gebruik van wag toe. In enige ander geval sal die gebruik van hierdie funksie 'n fout veroorsaak.

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

Async word heel aan die begin van die funksieverklaring ingevoeg, en in die geval van 'n pylfunksie, tussen die "="-teken en die hakies.

Hierdie funksies kan as metodes op 'n voorwerp geplaas word, of hulle kan in 'n klasverklaring gebruik word.

// 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! Dit is die moeite werd om te onthou dat klaskonstrukteurs en getters/setters nie asynchronies kan wees nie.

Semantiek en uitvoeringsreëls

Async-funksies is in beginsel soortgelyk aan standaard JS-funksies, maar daar is uitsonderings.

Dus, asinkroniseer funksies gee altyd beloftes:

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

In die besonder, fn gee die string hallo terug. Wel, aangesien dit 'n asynchrone funksie is, word die stringwaarde toegedraai in 'n belofte met behulp van 'n konstruktor.

Hier is 'n alternatiewe konstruksie sonder Async:

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

In hierdie geval word die terugkeer van die belofte "handmatig" gedoen. 'n Asinchroniese funksie is altyd in 'n nuwe belofte toegedraai.

In die geval dat die terugkeerwaarde 'n primitief is, gee die async-funksie die waarde terug en vou dit in 'n belofte. In die geval dat die teruggekeerde waarde 'n beloftevoorwerp is, word die oplossing daarvan in 'n nuwe belofte teruggestuur.

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

Maar wat gebeur as daar 'n fout in die asynchrone funksie is?

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

As dit nie verwerk word nie, sal foo() 'n verwerpte belofte terugstuur. In hierdie situasie, in plaas van Promise.resolve, sal 'n Promise.reject wat 'n fout bevat, teruggestuur word.

Async-funksies gee altyd 'n belofte terug, maak nie saak wat teruggestuur word nie.

Asinchroniese funksies word opgeskort op elke wag.

Wag beïnvloed uitdrukkings. Dus, as die uitdrukking 'n belofte is, word die asinkroniseringsfunksie opgeskort totdat die belofte vervul is. As die uitdrukking nie 'n belofte is nie, word dit omgeskakel na 'n belofte via Promise.resolve en dan beëindig.

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

En hier is 'n beskrywing van hoe die fn-funksie werk.

  • Nadat dit geroep is, word die eerste reël omgeskakel van const a = wag 9; in const a = wag Promise.resolve(9);.
  • Nadat u Wag gebruik het, word die uitvoering van die funksie opgeskort totdat a die waarde daarvan kry (in die huidige situasie is dit 9).
  • delayAndGetRandom(1000) onderbreek die fn-funksie totdat dit op sy eie eindig (na 1 sekonde). Dit stop die fn-funksie effektief vir 1 sekonde.
  • delayAndGetRandom(1000) deur resolve gee 'n ewekansige waarde terug, wat dan aan b toegeken word.
  • Wel, die geval met veranderlike c is soortgelyk aan die geval met veranderlike a. Daarna stop alles vir 'n sekonde, maar nou gee delayAndGetRandom(1000) niks terug nie aangesien dit nie nodig is nie.
  • As gevolg hiervan word die waardes volgens die formule a + b * c bereken. Die resultaat word toegedraai in 'n belofte met behulp van Promise.resolve en teruggestuur deur die funksie.

Hierdie pouses kan soortgelyk wees aan kragopwekkers in ES6, maar hierdie een het jou redes.

Ons los die probleem op

Wel, kom ons kyk nou na die oplossing vir die probleem wat hierbo aangedui is.

Die finishMyTask-funksie gebruik Await om te wag vir die resultate van bewerkings soos queryDatabase, sendEmail, logTaskInFile, en ander. As ons hierdie oplossing vergelyk met die een waar beloftes gebruik is, word die ooreenkoms duidelik. Die Async/Await-weergawe vereenvoudig egter die sintaktiese kompleksiteite nogal. In hierdie geval is daar nie baie terugbelopings en kettings soos .dan/.vang nie.

Hier is 'n oplossing met getalle-uitvoer, daar is twee opsies hier.

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

En hier is 'n oplossing met behulp van asynchrone funksies.

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

Kon nie verwerk nie

Onbehandelde foute word toegedraai in 'n verwerpte belofte. U kan egter die probeer/vang-konstruksie in asinkroniseerfunksies gebruik om sinchroniese fouthantering uit te voer.

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() is 'n asinchrone funksie wat óf slaag ("perfekte getal") of misluk met 'n fout ("Jammer, getal te groot").

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

Aangesien die voorbeeld hierbo van canRejectOrReturn verwag om uit te voer, sal die inheemse mislukking veroorsaak dat die vangblok uitgevoer word. As gevolg hiervan, sal foo óf eindig met ongedefinieerd (wanneer niks in die probeerblok teruggestuur word nie) óf met 'n fout wat opgevang is. As gevolg hiervan sal hierdie funksie nie misluk nie, want try / catch sal die foo-funksie self verwerk.

Hier is nog 'n voorbeeld:

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

Dit is die moeite werd om aandag te skenk aan die feit dat canRejectOrReturn van foo in die voorbeeld teruggestuur word. Foo voltooi in hierdie geval óf met 'n perfekte nommer óf gee 'n fout terug ("Jammer, nommer te groot"). Die vangblok sal nooit uitgevoer word nie.

Die probleem is dat foo die belofte terugstuur wat van canRejectOrReturn afgegee is. So die besluit van foo word die besluit van canRejectOrReturn. In hierdie geval sal die kode slegs uit twee reëls bestaan:

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

Maar wat gebeur as jy wag en saam terugkeer:

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

In die kode hierbo sal foo suksesvol voltooi met beide perfekte nommer en fout vasgevang. Hier sal geen mislukkings wees nie. Maar foo sal eindig met canRejectOrReturn, nie ongedefinieerd nie. Kom ons verifieer dit deur die terugkeer await canRejectOrReturn() reël te verwyder:

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

Algemene foute en slaggate

In sommige gevalle kan die gebruik van Async/Wag tot foute lei.

Vergete wag

Dit gebeur redelik gereeld - die wag sleutelwoord word vergeet voor die belofte:

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

Soos u kan sien, is daar nie wag of terugkeer in die kode nie. So gaan foo altyd uit met ongedefinieerde sonder 'n 1 sekonde vertraging. Maar die belofte sal vervul word. As dit 'n fout of 'n verwerping gooi, sal UnhandledPromiseRejectionWarning in hierdie geval geroep word.

Asinkroniseer funksies in terugbelopings

Async-funksies word dikwels in .map of .filter as terugroepe gebruik. 'n Voorbeeld is die fetchPublicReposCount(gebruikersnaam) funksie, wat die aantal bewaarplekke wat op GitHub oop is, terugstuur. Kom ons sê daar is drie gebruikers wie se maatstawwe ons wil hê. Hier is die kode vir hierdie taak:

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

Ons benodig ArfatSalman, octocat, norvig-rekeninge. In hierdie geval voer ons uit:

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

Let op die Wag in die .map terugbel. Hier tel 'n reeks beloftes, en .map is 'n anonieme terugbel vir elke gespesifiseerde gebruiker.

Oormatige konsekwente gebruik van wag

Kom ons neem hierdie kode as 'n voorbeeld:

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

Hier word die aantal repo's in die telveranderlike geplaas, dan word hierdie getal by die tellings-skikking gevoeg. Die probleem met die kode is dat totdat die data van die eerste gebruiker van die bediener af kom, alle daaropvolgende gebruikers in bystandmodus sal wees. Slegs een gebruiker word dus op 'n slag verwerk.

As dit byvoorbeeld ongeveer 300 ms neem om een ​​gebruiker te verwerk, dan is dit vir alle gebruikers reeds 'n sekonde, die tyd spandeer lineêr hang af van die aantal gebruikers. Maar aangesien die aantal repo's nie van mekaar afhang nie, kan die prosesse geparalleliseer word. Dit vereis om met .map en Promise.all te werk:

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

Promise.all neem 'n verskeidenheid beloftes as insette, wat 'n belofte teruggee. Die laaste een na voltooiing van alle beloftes in die skikking of by die eerste verwerping is voltooi. Dit kan gebeur dat almal nie op dieselfde tyd begin nie - om gelyktydige bekendstelling te verseker, kan jy p-map gebruik.

Gevolgtrekking

Async-funksies word al hoe belangriker vir ontwikkeling. Wel, vir die aanpasbare gebruik van asynchrone funksies, moet jy gebruik Async Iterators. 'n JavaScript-ontwikkelaar behoort goed hierin te wees.

Skillbox beveel aan:

Bron: will.com

Voeg 'n opmerking