Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Składnia async/await

W poprzednich ćwiczeniach poznaliśmy Promisy oraz metody ich składania, które znacznie ułatwiają pracę z kodem asynchronicznym. Jednak JavaScript oferuje jeszcze bardziej elegancką i czytelną składnię do obsługi asynchroniczności: async/await.

Składnia async/await, wprowadzona w ECMAScript 2017 (ES8), działa na bazie Promisów, ale pozwala pisać kod asynchroniczny w sposób, który wygląda jak synchroniczny. Dzięki temu nasz kod staje się bardziej czytelny i łatwiejszy w utrzymaniu - co jest niezwykle ważne przy zarządzaniu tak skomplikowanym przedsięwzięciem jak Park Jurajski!

Podstawy async/await

Składnia async/await składa się z dwóch głównych elementów:

  1. Słowo kluczowe async - używane przed deklaracją funkcji, oznacza, że funkcja zawsze zwróci Promise
  2. Słowo kluczowe await - używane wewnątrz funkcji async, wstrzymuje wykonanie funkcji do czasu rozwiązania Promise

Funkcje async

Zobaczmy, jak wygląda prosta funkcja async:

1// Funkcja tradycyjna zwracająca Promise
2function pobierzDinozaura(id) {
3  return new Promise((resolve, reject) => {
4    setTimeout(() => {
5      if (id > 0 && id <= 100) {
6        resolve({
7          id: id,
8          name: `Dino-${id}`,
9          species: id % 2 === 0 ? "Tyrannosaurus" : "Velociraptor",
10          health: "Good"
11        });
12      } else {
13        reject(new Error(`Dinozaur o ID ${id} nie istnieje`));
14      }
15    }, 1000);
16  });
17}
18
19// Funkcja async pobierająca dane dinozaura
20async function pobierzDaneDinozaura(id) {
21  console.log(`Rozpoczynam pobieranie danych dinozaura #${id}...`);
22
23  // Używamy await do "czekania" na wynik Promisa
24  const dinozaur = await pobierzDinozaura(id);
25
26  console.log(`Pobrano dane dinozaura: ${dinozaur.name}`);
27  return dinozaur;
28}
29
30// Wywołanie funkcji async
31pobierzDaneDinozaura(42)
32  .then(dino => {
33    console.log("Otrzymane dane:", dino);
34  })
35  .catch(error => {
36    console.error("Wystąpił błąd:", error.message);
37  });

Oto, co dzieje się w powyższym kodzie:

  1. Deklarujemy funkcję
    pobierzDaneDinozaura()
    ze słowem kluczowym
    async
    , co oznacza, że będzie ona zawsze zwracać Promise
  2. Wewnątrz tej funkcji używamy
    await
    przed wywołaniem
    pobierzDinozaura(id)
    , co powoduje, że wykonanie funkcji zostanie wstrzymane do czasu rozwiązania Promise
  3. Kiedy Promise zostanie rozwiązany, kod będzie kontynuował wykonywanie od miejsca, w którym został wstrzymany, a zmiennej
    dinozaur
    zostanie przypisana rozwiązana wartość Promise
  4. Ponieważ funkcja async zawsze zwraca Promise, możemy użyć metod
    .then()
    i
    .catch()
    przy wywołaniu
    pobierzDaneDinozaura()

Porównanie tradycyjnego podejścia z Promise i async/await

Porównajmy te same funkcjonalności, zaimplementowane z użyciem tradycyjnej składni Promise oraz async/await:

1// Z użyciem tradycyjnych Promise
2function sprawdźSystemyBezpieczeństwa() {
3  console.log("Sprawdzanie systemów bezpieczeństwa...");
4
5  return pobierzStatusBram()
6    .then(statusBram => {
7      console.log("Status bram:", statusBram.allClosed ? "Wszystkie zamknięte" : "UWAGA! Niektóre otwarte");
8
9      return pobierzStatusOgrodzenia();
10    })
11    .then(statusOgrodzenia => {
12      console.log("Ogrodzenie:", statusOgrodzenia.power > 8000 ? "Zabezpieczone" : "Niewystarczające zasilanie!");
13
14      return pobierzStatusKamer();
15    })
16    .then(statusKamer => {
17      console.log("Kamery:", statusKamer.allOperational ? "Wszystkie działają" : "Niektóre nie działają!");
18
19      return {
20        brama: statusBram,
21        ogrodzenie: statusOgrodzenia,
22        kamery: statusKamer,
23        czasSprawdzenia: new Date().toISOString()
24      };
25    })
26    .catch(error => {
27      console.error("Błąd podczas sprawdzania:", error.message);
28      throw new Error(`Sprawdzanie nie powiodło się: ${error.message}`);
29    });
30}
31
32// Z użyciem async/await
33async function sprawdźSystemyBezpieczeństwa() {
34  console.log("Sprawdzanie systemów bezpieczeństwa...");
35
36  try {
37    // Każde z poniższych wywołań wstrzymuje wykonanie funkcji
38    const statusBram = await pobierzStatusBram();
39    console.log("Status bram:", statusBram.allClosed ? "Wszystkie zamknięte" : "UWAGA! Niektóre otwarte");
40
41    const statusOgrodzenia = await pobierzStatusOgrodzenia();
42    console.log("Ogrodzenie:", statusOgrodzenia.power > 8000 ? "Zabezpieczone" : "Niewystarczające zasilanie!");
43
44    const statusKamer = await pobierzStatusKamer();
45    console.log("Kamery:", statusKamer.allOperational ? "Wszystkie działają" : "Niektóre nie działają!");
46
47    return {
48      brama: statusBram,
49      ogrodzenie: statusOgrodzenia,
50      kamery: statusKamer,
51      czasSprawdzenia: new Date().toISOString()
52    };
53  } catch (error) {
54    console.error("Błąd podczas sprawdzania:", error.message);
55    throw new Error(`Sprawdzanie nie powiodło się: ${error.message}`);
56  }
57}

Zauważ, że:

  • Kod wykorzystujący async/await jest znacznie bardziej czytelny - wygląda prawie jak kod synchroniczny
  • Łatwiej śledzić przepływ danych (wartości
    statusBram
    ,
    statusOgrodzenia
    ,
    statusKamer
    )
  • Obsługa błędów jest prostsza, wykorzystując standardową składnię
    try/catch
    zamiast
    .catch()

Zamiana funkcji zwracających Promise na async/await

Każdą funkcję, która zwraca Promise, możemy przekształcić na funkcję async:

1// Funkcja zwracająca Promise
2function pomiarTemperaturyWybiegu(wybiegId) {
3  return new Promise((resolve, reject) => {
4    // Kod pomiarowy...
5  });
6}
7
8// Przekształcenie na funkcję async
9async function pomiarTemperaturyWybiegu(wybiegId) {
10  // Możemy użyć operacji asynchronicznych z await
11  const czujnik = await połączZCzujnikiem(wybiegId);
12  const odczyt = await czujnik.pobierzTemperaturę();
13
14  if (odczyt.status === "error") {
15    throw new Error(`Błąd odczytu temperatury: ${odczyt.message}`);
16  }
17
18  return {
19    wybiegId,
20    temperatura: odczyt.value,
21    jednostka: "Celsius",
22    czasPomiaru: new Date()
23  };
24}

Równoległe wykonywanie operacji z async/await

Jedną rzeczą, na którą trzeba uważać przy używaniu async/await, jest sekwencyjność. Używając

await
przed każdym wywołaniem funkcji asynchronicznej, wykonujemy je jedna po drugiej, co nie zawsze jest pożądane.

Na przykład, w poniższym kodzie operacje są wykonywane sekwencyjnie, jedna po drugiej:

1async function inicjalizacjaSystemówParkuSekwencyjnie() {
2  console.log("Inicjalizacja systemów parku...");
3
4  const systemWodny = await inicjalizujSystemWodny();       // 3 sekundy
5  const systemZasilania = await inicjalizujSystemZasilania(); // 2 sekundy
6  const systemBezpieczeństwa = await inicjalizujSystemBezpieczeństwa(); // 4 sekundy
7
8  // Łączny czas: około 9 sekund
9  return {
10    systemWodny,
11    systemZasilania,
12    systemBezpieczeństwa,
13    status: "Wszystkie systemy gotowe"
14  };
15}

Jeśli operacje są niezależne od siebie, lepiej wykonać je równolegle:

1async function inicjalizacjaSystemówParkuRównolegle() {
2  console.log("Inicjalizacja systemów parku...");
3
4  // Uruchamiamy wszystkie operacje jednocześnie (bez await)
5  const obietnicaSystemuWodnego = inicjalizujSystemWodny();
6  const obietnicaSystemuZasilania = inicjalizujSystemZasilania();
7  const obietnicaSystemuBezpieczeństwa = inicjalizujSystemBezpieczeństwa();
8
9  // Czekamy na zakończenie wszystkich operacji
10  const [systemWodny, systemZasilania, systemBezpieczeństwa] = await Promise.all([
11    obietnicaSystemuWodnego,
12    obietnicaSystemuZasilania,
13    obietnicaSystemuBezpieczeństwa
14  ]);
15
16  // Łączny czas: około 4 sekundy (najdłuższa operacja)
17  return {
18    systemWodny,
19    systemZasilania,
20    systemBezpieczeństwa,
21    status: "Wszystkie systemy gotowe"
22  };
23}

W powyższym przykładzie wszystkie trzy inicjalizacje systemów są uruchamiane równolegle, a następnie używamy

Promise.all()
z
await
, aby poczekać na zakończenie wszystkich operacji. Dzięki temu możemy zredukować czas wykonania do czasu najdłuższej operacji, zamiast sumy czasów wszystkich operacji.

Obsługa błędów w async/await

Jak już widzieliśmy, jedną z głównych zalet składni async/await jest możliwość używania standardowej składni

try/catch
do obsługi błędów. Jest to znacznie bardziej czytelne niż łańcuchy
.catch()
w tradycyjnym podejściu Promise.

1async function wypuśćDinozauraZWybiegu(dinoId, wybiegId) {
2  try {
3    // Sprawdzenie, czy mamy uprawnienia
4    const uprawnienia = await sprawdźUprawnienia();
5    if (!uprawnienia.canReleaseAnimals) {
6      throw new Error("Brak uprawnień do wypuszczania dinozaurów");
7    }
8
9    // Sprawdzenie statusu dinozaura
10    const statusDinozaura = await sprawdźStatusDinozaura(dinoId);
11    if (statusDinozaura.health !== "Good") {
12      throw new Error(`Dinozaur #${dinoId} nie jest w dobrym stanie zdrowia`);
13    }
14
15    // Sprawdzenie, czy wybieg jest bezpieczny
16    const statusWybiegu = await sprawdźStatusWybiegu(wybiegId);
17    if (!statusWybiegu.isSecure) {
18      throw new Error(`Wybieg #${wybiegId} nie jest bezpieczny`);
19    }
20
21    // Otwieranie bramy wybiegu
22    await otwórzBramęWybiegu(wybiegId);
23    console.log(`Brama wybiegu #${wybiegId} otwarta`);
24
25    // Wypuszczenie dinozaura
26    const wynik = await przenieśDinozaura(dinoId, wybiegId, "outdoor-area");
27    console.log(`Dinozaur #${dinoId} został wypuszczony do strefy zewnętrznej wybiegu #${wybiegId}`);
28
29    return {
30      success: true,
31      message: `Dinozaur #${dinoId} został pomyślnie wypuszczony`,
32      details: wynik
33    };
34  } catch (error) {
35    console.error("Operacja wypuszczenia dinozaura nie powiodła się:", error.message);
36
37    // Możemy podjąć próbę naprawy błędu lub wykonać operacje czyszczące
38    try {
39      await zamknijWszystkieBramy();
40      await aktywujSystemBezpieczeństwa();
41    } catch (cleanupError) {
42      console.error("Błąd podczas operacji czyszczących:", cleanupError.message);
43    }
44
45    return {
46      success: false,
47      message: `Błąd podczas wypuszczania dinozaura: ${error.message}`,
48      error: error.message
49    };
50  }
51}

W powyższym przykładzie użyliśmy bloku

try/catch
do obsługi wszelkich błędów, które mogą wystąpić podczas procesu wypuszczania dinozaura. Możemy również zagnieżdżać bloki
try/catch
, jak widać w obsłudze błędu, gdzie próbujemy wykonać operacje czyszczące, które również mogą się nie powieść.

Asynchroniczne iteracje

Wprowadzenie async/await umożliwiło również asynchroniczne iteracje, które są szczególnie przydatne przy pracy z kolekcjami obiektów, które wymagają operacji asynchronicznych.

Sekwencyjne przetwarzanie kolekcji

1async function zbadajWszystkieDinozaury(listaId) {
2  console.log(`Rozpoczynam badanie ${listaId.length} dinozaurów...`);
3
4  const wyniki = [];
5
6  // Sekwencyjne przetwarzanie (jeden po drugim)
7  for (const id of listaId) {
8    try {
9      const wynik = await zbadajDinozaura(id);
10      console.log(`Dinozaur #${id} przebadany: ${wynik.status}`);
11      wyniki.push({
12        id,
13        success: true,
14        result: wynik
15      });
16    } catch (error) {
17      console.error(`Błąd badania dinozaura #${id}: ${error.message}`);
18      wyniki.push({
19        id,
20        success: false,
21        error: error.message
22      });
23    }
24  }
25
26  return {
27    łącznaBadań: listaId.length,
28    udanych: wyniki.filter(w => w.success).length,
29    nieudanych: wyniki.filter(w => !w.success).length,
30    wyniki
31  };
32}

W powyższym przykładzie używamy

for...of
z
await
wewnątrz pętli, co powoduje, że badania dinozaurów są wykonywane jedno po drugim. To jest odpowiednie podejście, gdy kolejność ma znaczenie lub gdy operacje muszą być wykonywane sekwencyjnie.

Równoległe przetwarzanie kolekcji

Jeśli operacje nie muszą być wykonywane sekwencyjnie, często lepiej jest przetwarzać kolekcję równolegle:

1async function zbadajWszystkieDinozauryRównolegle(listaId) {
2  console.log(`Rozpoczynam badanie ${listaId.length} dinozaurów równolegle...`);
3
4  // Tworzenie tablicy obietnic
5  const obietnice = listaId.map(id => {
6    return zbadajDinozaura(id)
7      .then(wynik => ({
8        id,
9        success: true,
10        result: wynik
11      }))
12      .catch(error => ({
13        id,
14        success: false,
15        error: error.message
16      }));
17  });
18
19  // Równoległe wykonanie wszystkich badań
20  const wyniki = await Promise.all(obietnice);
21
22  return {
23    łącznaBadań: listaId.length,
24    udanych: wyniki.filter(w => w.success).length,
25    nieudanych: wyniki.filter(w => !w.success).length,
26    wyniki
27  };
28}

W tym przypadku używamy

map()
do tworzenia tablicy Promise i następnie używamy
Promise.all()
z
await
, aby poczekać na zakończenie wszystkich operacji. Dzięki temu wszystkie badania są wykonywane równolegle, co może znacznie przyspieszyć przetwarzanie dużych kolekcji.

Równoległe przetwarzanie z ograniczeniem

Czasami nie chcemy wykonywać wszystkich operacji jednocześnie, bo mogłoby to przeciążyć system. W takim przypadku możemy ograniczyć liczbę równoległych operacji:

1async function zbadajDinozauryZOgraniczeniem(listaId, maxRównoległe = 3) {
2  console.log(`Rozpoczynam badanie ${listaId.length} dinozaurów (max ${maxRównoległe} równolegle)...`);
3
4  const wyniki = [];
5
6  // Przetwarzanie w porcjach
7  for (let i = 0; i < listaId.length; i += maxRównoległe) {
8    const porcja = listaId.slice(i, i + maxRównoległe);
9    console.log(`Przetwarzanie porcji ${Math.floor(i / maxRównoległe) + 1} (${porcja.length} dinozaurów)...`);
10
11    // Tworzenie tablicy obietnic dla bieżącej porcji
12    const obietnicePorcji = porcja.map(id => {
13      return zbadajDinozaura(id)
14        .then(wynik => ({
15          id,
16          success: true,
17          result: wynik
18        }))
19        .catch(error => ({
20          id,
21          success: false,
22          error: error.message
23        }));
24    });
25
26    // Równoległe wykonanie badań w bieżącej porcji
27    const wynikiPorcji = await Promise.all(obietnicePorcji);
28    wyniki.push(...wynikiPorcji);
29
30    console.log(`Porcja ${Math.floor(i / maxRównoległe) + 1} zakończona.`);
31  }
32
33  return {
34    łącznaBadań: listaId.length,
35    udanych: wyniki.filter(w => w.success).length,
36    nieudanych: wyniki.filter(w => !w.success).length,
37    wyniki
38  };
39}

W tym przypadku przetwarzamy listę ID w porcjach po

maxRównoległe
elementów. Każda porcja jest przetwarzana równolegle, ale czekamy na zakończenie wszystkich operacji w jednej porcji przed przejściem do następnej.

Async/await w funkcjach strzałkowych

Słowo kluczowe

async
może być również używane z funkcjami strzałkowymi:

1// Tradycyjna funkcja strzałkowa zwracająca Promise
2const monitorujDinozaura = (id) => {
3  return pobierzDinozaura(id)
4    .then(dino => {
5      return aktualizujStatusDinozaura(id, { lastChecked: new Date() });
6    });
7};
8
9// Funkcja strzałkowa async
10const monitorujDinozaura = async (id) => {
11  const dino = await pobierzDinozaura(id);
12  return await aktualizujStatusDinozaura(id, { lastChecked: new Date() });
13};

Async/await w metodach klasy

Możemy również używać

async
w metodach klas:

1class SystemMonitorowaniaDinozaurów {
2  constructor(sektor) {
3    this.sektor = sektor;
4    this.aktywny = false;
5    this.dinozaury = [];
6  }
7
8  async inicjalizuj() {
9    try {
10      // Pobieranie listy dinozaurów w sektorze
11      const lista = await pobierzListęDinozaurówWSektorze(this.sektor);
12      this.dinozaury = lista;
13
14      // Inicjalizacja czujników
15      await this.inicjalizujCzujniki();
16
17      this.aktywny = true;
18      console.log(`System monitorowania sektora ${this.sektor} zainicjalizowany`);
19
20      return true;
21    } catch (error) {
22      console.error(`Błąd inicjalizacji systemu monitorowania: ${error.message}`);
23      return false;
24    }
25  }
26
27  async inicjalizujCzujniki() {
28    // Inicjalizacja czujników dla każdego dinozaura
29    const obietnice = this.dinozaury.map(dino =>
30      inicjalizujCzujnikDlaDinozaura(dino.id)
31    );
32
33    return Promise.all(obietnice);
34  }
35
36  async monitoruj(czasMonitorowania = 60000) {
37    if (!this.aktywny) {
38      throw new Error("System monitorowania nie jest aktywny");
39    }
40
41    console.log(`Rozpoczynam monitorowanie sektora ${this.sektor} przez ${czasMonitorowania/1000} sekund...`);
42
43    // Monitorowanie przez określony czas
44    const startTime = Date.now();
45
46    while (Date.now() - startTime < czasMonitorowania) {
47      // Pobieranie statusu wszystkich dinozaurów
48      const statusy = await Promise.all(
49        this.dinozaury.map(dino => sprawdźStatusDinozaura(dino.id))
50      );
51
52      // Analiza statusów
53      const anormalneDinozaury = statusy.filter(s => s.status !== "normal");
54
55      if (anormalneDinozaury.length > 0) {
56        console.warn(`Wykryto ${anormalneDinozaury.length} dinozaurów z anormalnym statusem!`);
57
58        for (const status of anormalneDinozaury) {
59          console.warn(`Dinozaur #${status.dinoId}: ${status.status} - ${status.details}`);
60
61          // Wysłanie alertu dla każdego anormalnego statusu
62          await wyslijAlert(this.sektor, status);
63        }
64      }
65
66      // Czekamy 5 sekund przed następnym sprawdzeniem
67      await new Promise(resolve => setTimeout(resolve, 5000));
68    }
69
70    console.log(`Monitorowanie sektora ${this.sektor} zakończone.`);
71    return `Monitorowanie zakończone: ${new Date().toISOString()}`;
72  }
73}
74
75// Użycie klasy
76async function uruchomSystemMonitorowania(sektor) {
77  const system = new SystemMonitorowaniaDinozaurów(sektor);
78
79  const inicjalizacjaUdana = await system.inicjalizuj();
80  if (!inicjalizacjaUdana) {
81    console.error(`Nie udało się zainicjalizować systemu dla sektora ${sektor}`);
82    return;
83  }
84
85  // Monitorowanie przez 5 minut
86  await system.monitoruj(300000);
87
88  console.log(`Sesja monitorowania dla sektora ${sektor} zakończona`);
89}

W powyższym przykładzie mamy klasę z metodami asynchronicznymi, które używają async/await do obsługi operacji asynchronicznych.

Async IIFE (natychmiast wywołane funkcje asynchroniczne)

Czasami chcemy użyć async/await na najwyższym poziomie (np. w skrypcie), gdzie normalnie nie możemy używać await. W takich przypadkach możemy użyć IIFE (Immediately Invoked Function Expression) z async:

1// Natychmiast wywołana funkcja asynchroniczna
2(async () => {
3  try {
4    console.log("Uruchamianie systemu Parku Jurajskiego...");
5
6    // Inicjalizacja systemów
7    await inicjalizujWszystkieSystemy();
8
9    // Sprawdzanie statusu dinozaurów
10    const statusDinozaurów = await sprawdźWszystkieDinozaury();
11
12    // Logika na podstawie statusu
13    if (statusDinozaurów.wszystkieBezpieczne) {
14      console.log("Park jest gotowy do otwarcia dla zwiedzających.");
15      await otwórzParkDlaZwiedzających();
16    } else {
17      console.error("Park nie jest jeszcze gotowy. Wymagany przegląd bezpieczeństwa.");
18      await inicjujPrzeglądBezpieczeństwa();
19    }
20  } catch (error) {
21    console.error("Krytyczny błąd podczas uruchamiania systemu:", error);
22    await aktywujProtokolyAwaryjne();
23  }
24})();

Rekursywne funkcje asynchroniczne

Async/await może być również używane w funkcjach rekurencyjnych:

1// Funkcja rekurencyjna do próbowania aż do powodzenia
2async function spróbujAżDoSkutku(operacja, maxPrób = 5, opóźnienie = 1000) {
3  try {
4    return await operacja();
5  } catch (error) {
6    if (maxPrób <= 1) {
7      throw new Error(`Operacja nie powiodła się po wielu próbach: ${error.message}`);
8    }
9
10    console.log(`Próba nie powiodła się (pozostało ${maxPrób-1} prób). Czekam ${opóźnienie}ms...`);
11
12    // Czekamy określony czas
13    await new Promise(resolve => setTimeout(resolve, opóźnienie));
14
15    // Rekurencyjne wywołanie z jedną próbą mniej
16    return spróbujAżDoSkutku(operacja, maxPrób - 1, opóźnienie * 1.5);
17  }
18}
19
20// Przykład użycia
21async function uruchomDinozaura(id) {
22  await spróbujAżDoSkutku(
23    async () => {
24      const dino = await pobierzDinozaura(id);
25      if (!dino.awakened) {
26        await wybudźDinozaura(id);
27      }
28      return await aktualizujStatusDinozaura(id, { status: "active" });
29    },
30    3, // maksymalnie 3 próby
31    2000 // początkowe opóźnienie 2s (potem będzie 3s, potem 4.5s)
32  );
33
34  console.log(`Dinozaur #${id} został pomyślnie uruchomiony`);
35}

Async generatory i for-await-of

ECMAScript 2018 wprowadził asynchroniczne generatory i pętlę

for-await-of
, które są szczególnie przydatne do przetwarzania strumieni asynchronicznych danych:

1// Asynchroniczny generator dostarczający dane o dinozaurach
2async function* generatorDanychDinozaurów(sektor) {
3  const listaDinozaurów = await pobierzListęDinozaurówWSektorze(sektor);
4
5  for (const dino of listaDinozaurów) {
6    // Pobieranie szczegółowych danych dla każdego dinozaura
7    const szczegóły = await pobierzSzczegółyDinozaura(dino.id);
8
9    // Zwracamy dane dla każdego dinozaura osobno
10    yield {
11      ...dino,
12      szczegóły
13    };
14
15    // Symulujemy opóźnienie przed przejściem do następnego
16    await new Promise(resolve => setTimeout(resolve, 500));
17  }
18}
19
20// Użycie asynchronicznego generatora z for-await-of
21async function przetwórzDaneDinozaurów(sektor) {
22  console.log(`Rozpoczynam przetwarzanie danych dinozaurów z sektora ${sektor}...`);
23
24  for await (const dino of generatorDanychDinozaurów(sektor)) {
25    console.log(`Przetwarzam dane dinozaura: ${dino.name} (ID: ${dino.id})`);
26
27    // Przetwarzanie danych
28    await zapiszDaneDoRaportu(dino);
29
30    // Emisja alertu, jeśli dinozaur potrzebuje uwagi
31    if (dino.szczegóły.requiresAttention) {
32      await wyslijAlert(sektor, dino);
33    }
34  }
35
36  console.log(`Zakończono przetwarzanie danych dinozaurów z sektora ${sektor}`);
37}

W powyższym przykładzie używamy asynchronicznego generatora, który dostarcza dane o dinozaurach, a następnie używamy pętli

for-await-of
do przetwarzania tych danych. Jest to szczególnie przydatne, gdy mamy do czynienia z dużą ilością danych, które chcemy przetwarzać po kolei, ale gdzie każdy element wymaga operacji asynchronicznej.

Wzorce projektowe z async/await

Składnia async/await otwiera nowe możliwości dla wzorców projektowych w JavaScript. Oto kilka użytecznych wzorców:

Wzorzec retry (ponowne próby)

1async function zPonownymiPróbami(operacja, opcje = {}) {
2  const { maxPrób = 3, opóźnienie = 1000, backoffMnożnik = 2 } = opcje;
3
4  let ostatniError;
5
6  for (let próba = 1; próba <= maxPrób; próba++) {
7    try {
8      if (próba > 1) {
9        console.log(`Próba ${próba}/${maxPrób}...`);
10      }
11
12      return await operacja();
13    } catch (error) {
14      console.error(`Próba ${próba} nie powiodła się: ${error.message}`);
15      ostatniError = error;
16
17      if (próba < maxPrób) {
18        const czasOczekiwania = opóźnienie * Math.pow(backoffMnożnik, próba - 1);
19        console.log(`Oczekiwanie ${czasOczekiwania}ms przed kolejną próbą...`);
20        await new Promise(resolve => setTimeout(resolve, czasOczekiwania));
21      }
22    }
23  }
24
25  throw new Error(`Operacja nie powiodła się po ${maxPrób} próbach. Ostatni błąd: ${ostatniError.message}`);
26}
27
28// Przykład użycia
29async function podłączDoSystemuBezpieczeństwa() {
30  return zPonownymiPróbami(
31    async () => {
32      const połączenie = await nawiążPołączenieZSystememBezpieczeństwa();
33      const autentykacja = await uwierzytelnijPołączenie(połączenie);
34      return await pobierzStatusBezpieczeństwa(połączenie, autentykacja);
35    },
36    {
37      maxPrób: 5,
38      opóźnienie: 2000,
39      backoffMnożnik: 1.5
40    }
41  );
42}

Wzorzec timeout (limit czasu)

1async function zTimeoutem(operacja, limitCzasu, wiadomość) {
2  // Tworzymy Promise, który odrzuca się po przekroczeniu limitu czasu
3  const timeoutPromise = new Promise((_, reject) => {
4    setTimeout(() => {
5      reject(new Error(`Przekroczono limit czasu (${limitCzasu}ms): ${wiadomość}`));
6    }, limitCzasu);
7  });
8
9  // Uruchamiamy wyścig między operacją a timeoutem
10  return Promise.race([operacja(), timeoutPromise]);
11}
12
13// Przykład użycia
14async function bezpieczneOdczytywanieTemperatury(wybiegId) {
15  try {
16    const temperatura = await zTimeoutem(
17      () => odczytajTemperaturęWybiegu(wybiegId),
18      5000,
19      `Odczytywanie temperatury wybiegu #${wybiegId}`
20    );
21
22    console.log(`Temperatura wybiegu #${wybiegId}: ${temperatura}°C`);
23    return temperatura;
24  } catch (error) {
25    console.error(`Błąd odczytu temperatury: ${error.message}`);
26    return null; // Lub możemy zwrócić wartość domyślną
27  }
28}

Wzorzec retry z exponential backoff

1async function zExponentialBackoff(operacja, opcje = {}) {
2  const {
3    maxPrób = 5,
4    bazoweCzekanie = 1000,
5    maksymalneCzekanie = 30000,
6    jitter = true
7  } = opcje;
8
9  let próba = 0;
10
11  while (true) {
12    try {
13      return await operacja(++próba);
14    } catch (error) {
15      if (próba >= maxPrób) {
16        throw new Error(`Operacja nie powiodła się po ${próba} próbach: ${error.message}`);
17      }
18
19      // Obliczanie czasu oczekiwania z exponential backoff
20      let czasOczekiwania = Math.min(
21        maksymalneCzekanie,
22        bazoweCzekanie * Math.pow(2, próba - 1)
23      );
24
25      // Dodanie jittera (losowość) do czasu oczekiwania
26      if (jitter) {
27        czasOczekiwania = czasOczekiwania * (0.5 + Math.random() * 0.5);
28      }
29
30      console.log(`Próba ${próba} nie powiodła się. Oczekiwanie ${Math.round(czasOczekiwania)}ms przed kolejną próbą...`);
31
32      await new Promise(resolve => setTimeout(resolve, czasOczekiwania));
33    }
34  }
35}
36
37// Przykład użycia
38async function zsynchronizujDaneZCentralą() {
39  return zExponentialBackoff(
40    async (próba) => {
41      console.log(`Próba synchronizacji #${próba}...`);
42      const dane = await pobierzAktualneDane();
43      await wyślijDaneDoCentrali(dane);
44      return `Synchronizacja ukończona: ${dane.length} rekordów`;
45    },
46    {
47      maxPrób: 10,
48      bazoweCzekanie: 2000,
49      maksymalneCzekanie: 60000
50    }
51  );
52}

Wzorzec waterfalls (kaskadowe operacje)

1async function kaskadowePrzeszukiwanie(zapytanie) {
2  console.log(`Rozpoczynam kaskadowe przeszukiwanie dla: ${zapytanie}`);
3
4  // Próbujemy najpierw w pamięci podręcznej
5  try {
6    console.log("Przeszukiwanie pamięci podręcznej...");
7    const wynikCache = await przeszukajPamięćPodręczną(zapytanie);
8    console.log("Znaleziono w pamięci podręcznej!");
9    return {
10      źródło: "cache",
11      wynik: wynikCache
12    };
13  } catch (error) {
14    console.log("Nie znaleziono w pamięci podręcznej, próbuję lokalnej bazy danych...");
15  }
16
17  // Następnie próbujemy w lokalnej bazie danych
18  try {
19    const wynikLokalne = await przeszukajLokalnąBazęDanych(zapytanie);
20    console.log("Znaleziono w lokalnej bazie danych!");
21    // Aktualizacja pamięci podręcznej
22    await aktualizujPamięćPodręczną(zapytanie, wynikLokalne);
23    return {
24      źródło: "lokalna_baza",
25      wynik: wynikLokalne
26    };
27  } catch (error) {
28    console.log("Nie znaleziono w lokalnej bazie, próbuję w centralnej bazie...");
29  }
30
31  // W końcu próbujemy w centralnej bazie danych
32  try {
33    const wynikCentralne = await przeszukajCentralnąBazęDanych(zapytanie);
34    console.log("Znaleziono w centralnej bazie danych!");
35    // Aktualizacja lokalnej bazy i pamięci podręcznej
36    await aktualizujLokalnąBazęDanych(zapytanie, wynikCentralne);
37    await aktualizujPamięćPodręczną(zapytanie, wynikCentralne);
38    return {
39      źródło: "centralna_baza",
40      wynik: wynikCentralne
41    };
42  } catch (error) {
43    throw new Error(`Nie znaleziono wyników dla zapytania: ${zapytanie}`);
44  }
45}

Podsumowanie

Składnia async/await oferuje elegancki i czytelny sposób pracy z kodem asynchronicznym w JavaScript, który działa na bazie Promise. Oto główne zalety:

  1. Czytelność - kod wygląda bardziej jak synchroniczny, co ułatwia zrozumienie przepływu programu
  2. Obsługa błędów - możliwość używania standardowej składni
    try/catch
    zamiast
    .catch()
  3. Debugowanie - łatwiejsze śledzenie stosu wywołań i ustawianie breakpointów
  4. Kompozycja - łatwiejsze komponowanie złożonych operacji asynchronicznych

Pamiętaj jednak o kilku istotnych kwestiach:

  1. Funkcje async zawsze zwracają Promise - nawet jeśli nie używasz
    await
    wewnątrz funkcji
  2. Uważaj na sekwencyjność - używanie
    await
    przed każdym wywołaniem powoduje sekwencyjne wykonanie, co może nie być optymalne
  3. Nie zapomnij o obsłudze błędów - zawsze używaj
    try/catch
    lub
    .catch()
    przy wywołaniu funkcji async
  4. Używaj Promise.all() dla równoległego wykonania - gdy operacje są niezależne i mogą być wykonywane równolegle

W Parku Jurajskim, jak i w każdej złożonej aplikacji JavaScript, async/await jest nieocenionym narzędziem do zarządzania operacjami asynchronicznymi, takimi jak komunikacja z systemami bezpieczeństwa, monitorowanie dinozaurów, czy koordynacja różnych podsystemów parku.

W następnym ćwiczeniu skupimy się na strategiach obsługi błędów w kodzie asynchronicznym, co jest kluczowe dla tworzenia odpornych i niezawodnych aplikacji.

Ir a CodeWorlds