Pogledajmo Async/Await u JavaScriptu koristeći primjere

Autor članka ispituje primjere Async/Await u JavaScriptu. Općenito, Async/Await je prikladan način za pisanje asinkronog koda. Prije nego što se ova značajka pojavila, takav kod je bio napisan pomoću povratnih poziva i obećanja. Autor izvornog članka analizirajući razne primjere otkriva prednosti Async/Awaita.

Podsjećamo: za sve čitatelje "Habra" - popust od 10 000 rubalja pri upisu na bilo koji tečaj Skillbox koristeći promotivni kod "Habr".

Skillbox preporučuje: Edukativni online tečaj "Java programer".

Callback

Povratni poziv je funkcija čiji se poziv odgađa na neodređeno vrijeme. Prije su se povratni pozivi koristili u onim područjima koda gdje se rezultat nije mogao dobiti odmah.

Evo primjera asinkronog čitanja datoteke u Node.js:

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

Problemi nastaju kada trebate izvesti nekoliko asinkronih operacija odjednom. Zamislimo ovaj scenarij: upućen je zahtjev korisničkoj bazi podataka Arfat, trebate pročitati njeno polje profile_img_url i preuzeti sliku s servera someserver.com.
Nakon preuzimanja, sliku pretvaramo u drugi format, na primjer iz PNG u JPEG. Ako je konverzija bila uspješna, šalje se pismo na e-mail korisnika. Zatim se podaci o događaju unose u datoteku transformations.log, s naznakom datuma.

Vrijedno je obratiti pozornost na preklapanje povratnih poziva i veliki broj }) u završnom dijelu koda. Zove se Callback Hell ili Pyramid of Doom.

Nedostaci ove metode su očiti:

  • Ovaj kod je teško čitati.
  • Također je teško nositi se s pogreškama, što često dovodi do loše kvalitete koda.

Da bi se riješio ovaj problem, obećanja su dodana u JavaScript. Omogućuju vam da duboko ugniježđenje povratnih poziva zamijenite riječju .then.

Pozitivan aspekt obećanja je to što kod čine mnogo čitljivijim, odozgo prema dolje umjesto slijeva nadesno. Međutim, obećanja imaju i svojih problema:

  • Morate dodati mnogo .onda.
  • Umjesto try/catch, .catch se koristi za obradu svih grešaka.
  • Rad s više obećanja unutar jedne petlje nije uvijek prikladan; u nekim slučajevima kompliciraju kod.

Evo problema koji će pokazati značenje posljednje točke.

Pretpostavimo da imamo for petlju koja ispisuje niz brojeva od 0 do 10 u nasumičnim intervalima (0–n sekundi). Koristeći obećanja, trebate promijeniti ovu petlju tako da se brojevi ispisuju u nizu od 0 do 10. Dakle, ako je potrebno 6 sekundi za ispis nule i 2 sekunde za ispis jedinice, prvo bi se trebala ispisati nula, a zatim počet će odbrojavanje za ispis jednog.

I naravno, ne koristimo Async/Await ili .sort za rješavanje ovog problema. Primjer rješenja je na kraju.

Asinkrone funkcije

Dodatak asinkronih funkcija u ES2017 (ES8) pojednostavio je zadatak rada s obećanjima. Napominjem da asinkrone funkcije rade "povrh" obećanja. Ove funkcije ne predstavljaju kvalitativno različite pojmove. Asinkrone funkcije namijenjene su kao alternativa kodu koji koristi obećanja.

Async/Await omogućuje organizaciju rada s asinkronim kodom u sinkronom stilu.

Dakle, poznavanje obećanja olakšava razumijevanje principa Async/Await.

sintaksa

Obično se sastoji od dvije ključne riječi: async i await. Prva riječ pretvara funkciju u asinkronu. Takve funkcije dopuštaju korištenje čekanja. U svakom drugom slučaju, korištenje ove funkcije će generirati pogrešku.

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

Async se umeće na sam početak deklaracije funkcije, au slučaju funkcije strelice, između znaka “=” i zagrada.

Te se funkcije mogu smjestiti u objekt kao metode ili koristiti u deklaraciji klase.

// 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! Vrijedno je zapamtiti da konstruktori klasa i getteri/postavljači ne mogu biti asinkroni.

Semantika i pravila izvođenja

Asinkrone funkcije u osnovi su slične standardnim JS funkcijama, ali postoje iznimke.

Stoga asinkrone funkcije uvijek vraćaju obećanja:

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

Točnije, fn vraća niz hello. Pa, budući da je ovo asinkrona funkcija, vrijednost niza je umotana u obećanje pomoću konstruktora.

Evo alternativnog dizajna bez Async-a:

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

U ovom slučaju, obećanje se vraća "ručno". Asinkrona funkcija uvijek je umotana u novo obećanje.

Ako je vraćena vrijednost primitivna, asinkrona funkcija vraća vrijednost tako da je umota u obećanje. Ako je povratna vrijednost objekt obećanja, njegovo razrješenje se vraća u novom obećanju.

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

Ali što se događa ako postoji pogreška unutar asinkrone funkcije?

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

Ako se ne obradi, foo() će vratiti obećanje s odbijanjem. U ovoj situaciji, Promise.reject koji sadrži pogrešku bit će vraćen umjesto Promise.resolve.

Asinkrone funkcije uvijek izlaze obećanje, bez obzira na to što se vraća.

Asinkrone funkcije pauziraju pri svakom čekanju.

Čekanje utječe na izraze. Dakle, ako je izraz obećanje, asinkrona funkcija se obustavlja dok se obećanje ne ispuni. Ako izraz nije obećanje, pretvara se u obećanje putem Promise.resolve i zatim se dovršava.

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

A ovdje je opis kako funkcionira funkcija fn.

  • Nakon poziva, prvi redak se pretvara iz const a = await 9; in const a = await Promise.resolve(9);.
  • Nakon korištenja Await-a, izvršavanje funkcije se obustavlja dok a ne dobije svoju vrijednost (u trenutnoj situaciji to je 9).
  • delayAndGetRandom(1000) pauzira izvršenje funkcije fn dok se sama ne završi (nakon 1 sekunde). Ovo učinkovito zaustavlja funkciju fn na 1 sekundu.
  • delayAndGetRandom(1000) putem resolve vraća slučajnu vrijednost, koja se zatim dodjeljuje varijabli b.
  • Pa, slučaj s varijablom c sličan je slučaju s varijablom a. Nakon toga sve se zaustavlja na sekundu, ali sada delayAndGetRandom(1000) ne vraća ništa jer nije potrebno.
  • Kao rezultat toga, vrijednosti se izračunavaju pomoću formule a + b * c. Rezultat je umotan u obećanje pomoću Promise.resolve i vraća ga funkcija.

Ove pauze možda podsjećaju na generatore u ES6, ali ima nešto u tome tvoji razlozi.

Rješavanje problema

Pa, pogledajmo sada rješenje gore navedenog problema.

FinishMyTask funkcija koristi Await za čekanje rezultata operacija kao što su queryDatabase, sendEmail, logTaskInFile i druge. Usporedite li ovo rješenje s onim gdje su korištena obećanja, sličnosti će postati očite. Međutim, verzija Async/Await uvelike pojednostavljuje sve sintaktičke složenosti. U ovom slučaju nema velikog broja povratnih poziva i lanaca poput .then/.catch.

Ovdje je rješenje s izlazom brojeva, postoje dvije mogućnosti.

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

A ovdje je rješenje koje koristi asinkrone funkcije.

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

Greška u obradi

Neobrađene pogreške umotane su u odbijeno obećanje. Međutim, asinkrone funkcije mogu koristiti try/catch za sinkrono rukovanje pogreškama.

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() je asinkrona funkcija koja ili uspijeva ("savršen broj") ili ne uspijeva uz pogrešku ("Žao nam je, broj je prevelik").

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

Budući da gornji primjer očekuje izvršenje canRejectOrReturn, njegov vlastiti kvar rezultirat će izvršenjem bloka catch. Kao rezultat toga, funkcija foo završit će s nedefiniranim (kada ništa nije vraćeno u bloku pokušaja) ili s uhvaćenom pogreškom. Kao rezultat, ova funkcija neće uspjeti jer će try/catch sama obraditi funkciju foo.

Evo još jednog primjera:

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

Vrijedno je obratiti pozornost na činjenicu da se u primjeru canRejectOrReturn vraća iz foo. Foo u ovom slučaju ili završava savršenim brojem ili vraća pogrešku ("Oprostite, broj je prevelik"). Blok catch nikada se neće izvršiti.

Problem je u tome što foo vraća obećanje proslijeđeno iz canRejectOrReturn. Tako rješenje za foo postaje rješenje za canRejectOrReturn. U ovom slučaju, kod će se sastojati od samo dva retka:

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

Evo što se događa ako zajedno koristite čekanje i povratak:

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

U gornjem kodu, foo će uspješno izaći sa savršenim brojem i uhvaćenom pogreškom. Ovdje neće biti odbijanja. Ali foo će se vratiti s canRejectOrReturn, a ne s undefined. Uvjerimo se u ovo uklanjanjem retka return await canRejectOrReturn():

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

Uobičajene pogreške i zamke

U nekim slučajevima korištenje Async/Await može dovesti do pogrešaka.

Zaboravljeno čekanje

To se događa prilično često - ključna riječ čekanja zaboravljena je prije obećanja:

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

Kao što vidite, u kodu nema čekanja ili povratka. Stoga foo uvijek izlazi s nedefiniranim bez odgode od 1 sekunde. Ali obećanje će biti ispunjeno. Ako izbaci pogrešku ili odbije, tada će biti pozvan UnhandledPromiseRejectionWarning.

Asinkrone funkcije u povratnim pozivima

Asinkrone funkcije se često koriste u .map ili .filter kao povratni pozivi. Primjer je funkcija fetchPublicReposCount(username) koja vraća broj otvorenih repozitorija na GitHubu. Recimo da postoje tri korisnika čije metrike trebamo. Evo koda za ovaj zadatak:

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

Trebamo ArfatSalman, octocat, norvig račune. U ovom slučaju radimo:

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

Vrijedno je obratiti pozornost na Await u povratnom pozivu .map. Ovdje se broji niz obećanja, a .map je anonimni povratni poziv za svakog navedenog korisnika.

Pretjerano dosljedna upotreba čekanja

Uzmimo ovaj kod kao primjer:

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

Ovdje se repo broj stavlja u varijablu count, zatim se ovaj broj dodaje nizu counts. Problem s kodom je taj što dok podaci prvog korisnika ne stignu sa servera, svi sljedeći korisnici će biti u stanju mirovanja. Dakle, samo jedan korisnik se obrađuje u jednom trenutku.

Ako je npr. za obradu jednog korisnika potrebno oko 300 ms, onda je za sve korisnike to već sekunda, utrošeno vrijeme linearno ovisi o broju korisnika. Ali budući da dobivanje broja repoa ne ovisi jedno o drugom, procesi se mogu paralelizirati. Ovo zahtijeva rad s .map i 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 prima niz obećanja kao ulaz i vraća obećanje. Potonji se dovršava nakon što se dovrše sva obećanja u nizu ili pri prvom odbijanju. Može se dogoditi da se svi ne pokrenu u isto vrijeme - kako biste osigurali istodobni početak, možete koristiti p-mapu.

Zaključak

Asinkrone funkcije postaju sve važnije za razvoj. Pa, za prilagodljivu upotrebu asinkronih funkcija, trebali biste koristiti Asinkroni iteratori. Programer JavaScripta trebao bi biti dobro upućen u ovo.

Skillbox preporučuje:

Izvor: www.habr.com

Dodajte komentar