Pogledajmo Async/Await u JavaScriptu koristeći primjere

Autor članka istražuje primjere Async/Await u JavaScript-u. Sve u svemu, Async/Await je zgodan način za pisanje asinhronog koda. Prije nego što se ova funkcija pojavila, takav kod je napisan korištenjem povratnih poziva i obećanja. Autor originalnog članka otkriva prednosti Async/Await analizom različitih primjera.

Podsećamo: za sve čitaoce "Habra" - popust od 10 rubalja pri upisu na bilo koji Skillbox kurs koristeći "Habr" promotivni kod.

Skillbox preporučuje: Obrazovni online kurs "Java programer".

povratni

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

Evo primjera asinhronog čitanja datoteke u Node.js:

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

Problemi nastaju kada trebate izvršiti nekoliko asinhronih operacija odjednom. Zamislimo ovaj scenario: napravljen je zahtjev bazi podataka korisnika Arfata, potrebno je pročitati njeno polje profile_img_url i preuzeti sliku sa servera someserver.com.
Nakon preuzimanja, pretvaramo sliku u drugi format, na primjer iz PNG u JPEG. Ako je konverzija bila uspješna, na e-mail korisnika se šalje pismo. Zatim se informacije o događaju unose u datoteku transformations.log, navodeći datum.

Vrijedi obratiti pažnju na preklapanje povratnih poziva i veliki broj }) u završnom dijelu koda. Zove se pakao povratnog poziva ili piramida propasti.

Nedostaci ove metode su očigledni:

  • Ovaj kod je teško čitati.
  • Takođe je teško rukovati greškama, što često dovodi do lošeg kvaliteta koda.

Da bi se riješio ovaj problem, JavaScriptu su dodana obećanja. Oni vam omogućavaju da zamijenite duboko ugniježđenje povratnih poziva riječju .then.

Pozitivan aspekt obećanja je da čine kod mnogo bolje čitljivijim, odozgo prema dolje, a ne s lijeva na desno. Međutim, obećanja imaju i svoje probleme:

  • Morate dodati puno .onda.
  • Umjesto try/catch, .catch se koristi za rukovanje svim greškama.
  • Rad s više obećanja unutar jedne petlje nije uvijek zgodan, u nekim slučajevima oni komplikuju kod.

Evo problema koji će pokazati značenje posljednje tač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, morate promijeniti ovu petlju tako da se brojevi ispisuju u nizu od 0 do 10. Dakle, ako je potrebno 6 sekundi da se ispiše nula i 2 sekunde da se ispiše jedinica, prvo treba ispisati nulu, a zatim počinje odbrojavanje za štampanje.

I naravno, ne koristimo Async/Await ili .sort da riješimo ovaj problem. Primjer rješenja je na kraju.

Async funkcije

Dodavanje async funkcija u ES2017 (ES8) pojednostavilo je zadatak rada s obećanjima. Napominjem da async funkcije rade "na vrhu" obećanja. Ove funkcije ne predstavljaju kvalitativno različite koncepte. Async funkcije su zamišljene kao alternativa kodu koji koristi obećanja.

Async/Await omogućava organizovanje rada sa asinhronim kodom u sinkronom stilu.

Stoga, 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 omogućavaju upotrebu čekanja. U svakom drugom slučaju, korištenje ove funkcije će generirati greš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 ubacuje na sam početak deklaracije funkcije, au slučaju funkcije sa strelicom, između znaka “=” i zagrada.

Ove funkcije se mogu postaviti 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! Vrijedi zapamtiti da konstruktori klasa i getteri/setteri ne mogu biti asinhroni.

Semantika i pravila izvršenja

Async funkcije su u osnovi slične standardnim JS funkcijama, ali postoje izuzeci.

Dakle, async funkcije uvijek vraćaju obećanja:

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

Konkretno, fn vraća string hello. Pa, pošto je ovo asinhrona funkcija, vrijednost stringa je umotana u obećanje pomoću konstruktora.

Evo alternativnog dizajna bez Async:

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

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

Ako je povratna vrijednost primitivna, async funkcija vraća vrijednost umotavanjem u obećanje. Ako je povratna vrijednost objekt obećanja, njegova rezolucija 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 greška unutar asinkrone funkcije?

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

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

Async funkcije uvijek daju obećanje, bez obzira na to što se vraća.

Asinkrone funkcije pauziraju na svakom čekanju.

Čekanje utiče na izraze. Dakle, ako je izraz obećanje, async funkcija se suspenduje dok se obećanje ne ispuni. Ako izraz nije obećanje, pretvara se u obećanje putem Promise.resolve i zatim završ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 evo i opisa kako funkcioniše funkcija fn.

  • Nakon što ga pozovete, prvi red se konvertuje iz const a = await 9; in const a = await Promise.resolve(9);.
  • Nakon upotrebe Await, izvršenje funkcije se suspenduje dok a ne dobije svoju vrijednost (u trenutnoj situaciji je 9).
  • delayAndGetRandom(1000) pauzira izvršavanje funkcije fn dok se ne završi (nakon 1 sekunde). Ovo efektivno zaustavlja funkciju fn na 1 sekundu.
  • delayAndGetRandom(1000) putem resolve vraća nasumične vrijednosti, koja se zatim dodjeljuje varijabli b.
  • Pa, slučaj sa varijablom c je sličan slučaju sa 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 koristeći Promise.resolve i vraća ga funkcija.

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

Rješavanje problema

Pa, sada pogledajmo rješenje za gore spomenuti problem.

Funkcija finishMyTask koristi Await za čekanje rezultata operacija kao što su queryDatabase, sendEmail, logTaskInFile i druge. Ako uporedite ovo rješenje s onim u kojem su korištena obećanja, sličnosti će postati očigledne. Međutim, verzija Async/Await uvelike pojednostavljuje sve sintaksičke složenosti. U ovom slučaju, nema velikog broja povratnih poziva i lanaca poput .then/.catch.

Evo rješenja sa izlazom brojeva, postoje dvije opcije.

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 evo rješenja koje koristi asinhronizirane 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 greške su umotane u odbijeno obećanje. Međutim, asinhrone funkcije mogu koristiti try/catch za sinhrono rukovanje greš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 asinhrona funkcija koja ili uspijeva („savršen broj“) ili ne uspijeva s greškom („Izvinite, broj je prevelik“).

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

Budući da gornji primjer očekuje da se canRejectOrReturn izvrši, njegov vlastiti neuspjeh će rezultirati izvršenjem catch bloka. Kao rezultat, funkcija foo će završiti ili sa nedefiniranom (kada ništa nije vraćeno u bloku try) ili s uhvaćenom greškom. Kao rezultat, ova funkcija neće uspjeti jer će try/catch sama rukovati funkcijom foo.

Evo još jednog primjera:

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

Vrijedi obratiti pažnju na činjenicu da se u primjeru canRejectOrReturn vraća iz foo. Foo u ovom slučaju ili završava sa savršenim brojem ili vraća grešku ("Izvinite, broj je prevelik"). Blok catch se nikada neće izvršiti.

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

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

Evo šta se dešava ako koristite čekanje i povratak zajedno:

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

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

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

Uobičajene greške i zamke

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

Zaboravljeno čekati

Ovo se dešava prilično često - ključna riječ await se zaboravlja prije obećanja:

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

Kao što vidite, u kodu nema čekanja ili vraćanja. Stoga foo uvijek izlazi sa nedefiniranim bez kašnjenja od 1 sekunde. Ali obećanje će biti ispunjeno. Ako izbaci grešku ili odbacivanje, tada će biti pozvan UnhandledPromiseRejectionWarning.

Asinhronizirane funkcije u povratnim pozivima

Async funkcije se često koriste u .map ili .filter kao povratni pozivi. Primjer je funkcija fetchPublicReposCount(username), koja vraća broj otvorenih spremišta na GitHubu. Recimo da postoje tri korisnika čije su nam metrike potrebne. 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'];
}

Potrebni su nam ArfatSalman, oktokat, norvig računi. U ovom slučaju radimo:

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

Vrijedi obratiti pažnju na Await u povratnom pozivu .map. Ovdje counts predstavlja niz obećanja, a .map je anonimni povratni poziv za svakog određenog korisnika.

Previše 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, a zatim se ovaj broj dodaje nizu counts. Problem sa kodom je u tome što sve dok podaci prvog korisnika ne stignu sa servera, svi naredni korisnici će biti u stanju pripravnosti. Dakle, istovremeno se obrađuje samo jedan korisnik.

Ako je, na primjer, potrebno oko 300 ms za obradu jednog korisnika, onda je to za sve korisnike već sekunda koja linearno ovisi o broju korisnika. Ali pošto dobijanje broja repo ne zavisi jedno od drugog, procesi se mogu paralelizirati. Ovo zahtijeva rad sa .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. Potonje, nakon što su sva obećanja u nizu završena ili pri prvom odbijanju, je završena. Može se dogoditi da svi ne počnu u isto vrijeme - kako biste osigurali istovremeni početak, možete koristiti p-map.

zaključak

Asinhronizirane funkcije postaju sve važnije za razvoj. Pa, za adaptivno korištenje asinhroniziranih funkcija vrijedi ga koristiti Async Iteratori. JavaScript programer bi trebao biti dobro upućen u ovo.

Skillbox preporučuje:

izvor: www.habr.com

Dodajte komentar