Asynchroniczność w Node.js

Promises

Obiekty promise (pol. obietnica) reprezentują operacje asynchroniczne, które mogą w pewnym momencie zakończyć się i zwrócić jakąś wartość. Promise „obiecuje” (albo „zapowiada”) wartość, o której nie wiadomo, w którym momencie stanie się dostępna. Przetwarzanie wartości „obiecanych” przez promise odbywa się asynchronicznie, wtedy, kiedy wartości te stają się dostępne. Kod programu opartego na promises może przypominać prosty kod synchroniczny i to stanowi jedną z głównych zalet tej techniki programowania asynchronicznego.


Przykładowo, pracę z promises umożliwia API promises modułu fs, dostępne w Node.js jako alternatywa dla „tradycyjnych” funkcji modułu fs (w których asynchroniczność jest realizowana za pomocą funkcji callback). Funkcja readFile, zawarta w API promises, zwraca obiekt promise:

const { readFile } = require('fs').promises;

// Utworzenie obiektu promise, reprezentującego "obietnicę" wczytania pliku
const promise = readFile("my_file.txt", "utf8");

Aby określić, co ma się dziać po „spełnieniu obietnicy” – czyli wczytaniu pliku – można wykorzystać metodę then, która jako argument przyjmuje funkcję:

promise.then((data) => {
  // Poniższa instrukcja zostanie wykonana po spełnieniu obietnicy,
  // czyli po wczytaniu pliku
  console.log(`Zawartość pliku:\n${data}`);
});

Aby określić, co ma się dziać, jeśli obietnicy nie uda się spełnić (czyli zostanie odrzucona – np. w przypadku, gdy podana zostanie błędna nazwa pliku), można wykorzystać metodę catch:

promise.then((data) => {
  console.log(`Zawartość pliku:\n${data}`);
}).catch((error) => {
  // Poniższa instrukcja zostanie wykonana, jeśli obietnica
  // zostanie odrzucona (rejected), tzn. nie uda się wczytać pliku
  console.error(`Nie udało się wczytać pliku, bo:\n${error}`);
});

async, await

Słowa async i await umożliwiają wygodny sposób pracy z obiektami promise, nie wymagający ani tworzenia ich wprost, ani jawnego wywoływania metody then.

Poprzedzenie definicji funkcji słowem async sprawia, że funkcja ta będzie zwracała obiekt promise (a co za tym idzie, jej działanie będzie realizowane asynchronicznie):

// Funkcja main będzie zwracała promise
async function main() { ... }

Wewnątrz funkcji opatrzonej słowem async można wykorzystywać słowo await, aby sprawić, że program będzie „oczekiwał” na spełnienie jakiejś promise, zanim wykonane zostaną jego kolejne instrukcje:

const { readFile } = require('fs').promises;

async function main() {

  const data = await readFile("my_file.txt", "utf8");
  
  // Poniższa instrukcja zostanie wywołana dopiero
  // po spełnieniu obietnicy przez readFile
  console.log(`Zawartość pliku:\n${data}`);
}

main()

Użycie słowa async nieodłącznie oznacza użycie obiektu promise. Program przedstawiony w powyższym przykładzie jest – z grubsza – równoważny programowi przedstawionemu wcześniej, korzystającemu z metody then.

Za pomocą słów async, await można w czytelny sposób zrealizować serię operacji asynchronicznych, z których każda korzysta z poprzednich wyników:

async function zróbCośZłożonego(dane) {
  wynik1 = await operacja1(dane)
  wynik2 = await operacja2(wynik1)
  wynik3 = await operacja3(wynik2)
  operacja4(wynik3);
}

Jedną z głównych zalet promises realizowanych za pomocą słów async i await jest możliwość pisania kodu asynchronicznego, który przypomina kod synchroniczny (a więc jest czytelny), jak w przykładzie powyżej.


Stany promises

Obiekt promise znajduje się zawsze w jednym z trzech stanów:

  • nierozstrzygnięty (ang. pending),
  • spełniony (ang. fulfilled),
  • odrzucony (ang. rejected).

Stan pending jest początkowym stanem każdej promise. Oznacza, że „prace trwają” (albo nawet jeszcze się nie zaczęły) i nie wiadomo, jaki będzie ostateczny stan i wartość tej promise, ani kiedy ten stan i wartość zostaną określone.

Stan fulfilled oznacza, że zapowiadana operacja została zakończona i dostępna jest „obiecana” wartość (fulfillment value).

Stan rejected oznacza, że zapowiadana operacja nie powiodła się, ale dostępna jest informacja o przyczynie odrzucenia (rejection reason – coś w rodzaju wyjątku, który można „przechwycić”).

W kontekście promises pojawia się często określenie resolve. W szczególności, istnieje użyteczna funkcja Promise.resolve. Wbrew pozorom, określenie resolve niekoniecznie oznacza „spełnienie” promise w sensie wprowadzenia jej w stan fulfilled. Promise może być resolved jakiś czas przed „rozstrzygnięciem” jej stanu, może być równocześnie resolved i pending, a także równocześnie resolved i rejected.

To, że promise jest resolved, oznacza że:

  • albo dana promise jest „rozstrzygnięta”, czyli jest fulfilled z określoną wartością lub rejected z określonym powodem;
  • albo docelowy stan i wartość danej promise zależy wyłącznie od docelowego (nieznanego jeszcze) stanu i wartości jakiejś innej, „nierozstrzygniętej” promise.

Literatura