Skip to content

11. Programowanie zdarzeniowe i funkcje asynchroniczne

Image

Funkcja asynchroniczna to funkcja, której wykonanie zostało uruchomione, ale na której wynik nie jest oczekiwany. Po zakończeniu wykonywania funkcja asynchroniczna generuje zdarzenie i przekazuje swój wynik za jego pośrednictwem.

Ten sposób działania doskonale nadaje się do wykonywania w przeglądarce internetowej. Aplikacja uruchomiona w przeglądarce jest bowiem aplikacją zdarzeniową: reaguje na zdarzenia, głównie wywoływane przez użytkownika (kliknięcia, ruchy myszą, wpisywanie tekstu itp.). Aplikacje JavaScript uruchamiane w przeglądarce muszą komunikować się z usługami zewnętrznymi za pośrednictwem protokołu HTTP. Natywne funkcje JavaScript o nazwie HTTP działają asynchronicznie: są uruchamiane, a otrzymanie odpowiedzi od wywołanej usługi zewnętrznej jest sygnalizowane przez zdarzenie, które dołącza się do zestawu zdarzeń obsługiwanych przez aplikację.

Poniższe skrypty będą wykonywane przez [node.js], a nie przez przeglądarkę. [node.js] również działa w trybie zdarzeniowym:

  • [node.js], podobnie jak przeglądarka, wykorzystuje pętlę zdarzeń do wykonania skryptu;
  • wykonanie głównego kodu skryptu jest pierwszym wykonywanym zdarzeniem;
  • jeśli ten główny kod uruchomił zadania asynchroniczne, to wykonywanie trwa do momentu zakończenia tych zadań. Zadania te generują zdarzenie po zakończeniu. Zdarzenia te są umieszczane w kolejce w pętli zdarzeń;
  • skrypt główny musi subskrybować te zdarzenia, jeśli chce uzyskać wyniki działań asynchronicznych;
  • skrypt jest zakończony dopiero wtedy, gdy wszystkie wygenerowane przez niego zdarzenia zostały przetworzone;

11.1. skrypt [async-01]

Poniższy skrypt ilustruje zachowanie skryptu zawierającego akcję asynchroniczną.


'use strict';

// importy
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// początek
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// setTimeout uruchamia licznik czasu o wartości 1000 ms (drugi parametr) i natychmiast zwraca numer tego licznika
// gdy licznik czasu upłynie 1000 ms, generuje zdarzenie, które jest umieszczane w kolejce środowiska uruchomieniowego
// gdy zdarzenie zostanie przetworzone przez środowisko uruchomieniowe, wykonywana jest funkcja (pierwszy parametr)
setTimeout(function () {
  // kod ten zostanie wykonany, gdy licznik czasu osiągnie wartość 0
  console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)

// zostanie wyświetlone przed komunikatem funkcji wewnętrznej timera
console.log("[fin du code principal du script],", heure(débutScript));

// narzędzie do wyświetlania godziny i czasu trwania
function heure(début) {
  // aktualna godzina
  const now = moment(Date.now());
  // formatowanie godziny
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // czy należy obliczyć czas trwania?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatowanie godziny i czasu trwania
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // wynik
  return result;
}
  • wiersz 4: importujemy bibliotekę [moment], aby móc formatować daty (wiersz 27);
  • wiersz 5: importuje się bibliotekę [sprintf-js] w celu formatowania czasów trwania (wiersz 34);
  • wiersz 8: zapisujemy godzinę rozpoczęcia działania skryptu;
  • wiersz 9: wyświetla się ją za pomocą metody [heure] z wierszy 20–34;
  • wiersze 14–17: funkcja [setTimeout] ma dwa parametry (f, czas trwania): f to funkcja wykonywana po upływie [durée] milisekund;
  • wiersz 14: podczas wykonywania skryptu uruchamiana jest funkcja [setTimeout]:
    • wiersz 17: uruchamiany jest licznik czasu o wartości 1000 ms i rozpoczyna się odliczanie do wartości 0. Funkcja [setTimeout] kończy działanie natychmiast po zainicjowaniu licznika i rozpoczęciu odliczania. Nie czeka ona na zakończenie tego odliczania. Zwraca numer identyfikujący używany licznik czasu, a wykonanie przechodzi do następnej instrukcji, w wierszu 20. W tym miejscu wynik funkcji [setTimeout] nie jest wykorzystywany;
  • wiersz 16: ten komunikat zostanie wyświetlony po upływie 1000 ms w funkcji [setTimeout];
  • wiersze 15–16: funkcja f, pierwszy parametr funkcji [setTimeout], zostanie wykonana po upływie 1000 ms. Wówczas wyświetli się komunikat z wiersza 16;
  • wiersz 20: komunikat ten zostanie wyświetlony przed komunikatem z wiersza 16;

Funkcja czasu:

  • wiersz 23: funkcja przyjmuje opcjonalny parametr [heure], który określa czas rozpoczęcia operacji, której czas trwania ma wyświetlić;
  • wiersze 25–27: oblicza się i formatuje aktualny czas;
  • wiersz 29: jeśli występuje parametr [début], należy obliczyć czas trwania;
  • wiersz 30: czas trwania operacji. Otrzymujemy liczbę milisekund;
  • wiersze 31–32: ta liczba milisekund jest rozkładana na sekundy i milisekundy;
  • wiersz 34: czas trwania jest dodawany do godziny;

Wykonanie


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-01.js"
[début du script], heure=09:26:40:238
[fin du code principal du script], heure=09:26:40:246, durée= 0 seconde(s) et 11 millisecondes
[fin de l'action asynchrone setTimeout], heure=09:26:41:249, durée= 1 seconde(s) et 14 millisecondes

[Done] exited with code=0 in 1.672 seconds
  • w wierszu 4 widać, że operacja asynchroniczna [setTimeOut] zakończyła się około 1 s po zakończeniu głównego kodu skryptu;
  • wiersz 6: czas wyświetlony w wierszu 3 to czas zakończenia kodu głównego. Jeśli kod ten uruchomił zadania asynchroniczne, skrypt kończy się dopiero po wykonaniu wszystkich zadań asynchronicznych. Czas wyświetlony w wierszu 6 to całkowity czas wykonania skryptu (kod główny + zadania asynchroniczne);

Funkcja [setTimeout] pozwoli nam symulować zadania asynchroniczne w środowisku [node.js]. Funkcja [setTimeout] zachowuje się bowiem jak zadanie asynchroniczne:

  • zwraca wynik natychmiast, w tym przypadku numer timera, za pomocą standardowego mechanizmu funkcji (return);
  • może ona później (co nie ma jeszcze miejsca w powyższym przykładzie) zwrócić inne wyniki za pośrednictwem zdarzeń, które są następnie przetwarzane przez pętlę zdarzeń funkcji [node.js];
  • w większości przypadków, które nastąpią później, zdarzeń tych będzie dwa:
    • zdarzenie, które można by nazwać [success], które zostanie wygenerowane przez zadanie asynchroniczne, które pomyślnie wykonało swoje zadanie. Z wygenerowanym zdarzeniem powiązana jest dana wartość, czyli wynik zadania;
    • zdarzenie, które można by nazwać [failure], które zostanie wygenerowane przez zadanie asynchroniczne, któremu nie udało się wykonać tego, co miało wykonać. Z wygenerowanym zdarzeniem powiązana jest dana informacja – zazwyczaj obiekt opisujący błąd. Przykładowe błędy, które mogą wystąpić w przypadku asynchronicznego zadania internetowego, to „sieć niedostępna”, „serwer nie istnieje”, „przekroczono limit czasu” itp.
  • Główny kod, który uruchomił zadanie asynchroniczne, może subskrybować zdarzenia, które to zadanie może wygenerować. Gdy jedno z nich zostanie wygenerowane, główny kod otrzymuje powiadomienie i może uruchomić wykonanie specjalnej funkcji przeznaczonej do obsługi tego zdarzenia. Funkcja ta otrzymuje jako parametr dane, które zadanie asynchroniczne powiązało z wygenerowanym zdarzeniem;

11.2. skrypt [async-02]

W tym skrypcie funkcja asynchroniczna [setTimeout] będzie generować zdarzenia w celu przekazania danych do kodów, które je subskrybują.

Dostęp do zdarzeń funkcji [node.js] wymaga dodatkowych bibliotek. Wybieramy bibliotekę [events], którą instalujemy wraz z [npm]:

Image

Skrypt [async-02] wygląda następująco:


'use strict';

// funkcje asynchroniczne mogą zwracać wynik poprzez wygenerowanie zdarzenia
// kod główny może pobrać te wyniki, subskrybując wysyłane zdarzenia

// importy
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// początek
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// emiter zdarzeń
const eventEmitter = new EventEmitter();

// setTimeout uruchamia licznik czasu o wartości 1000 ms (drugi parametr) i natychmiast zwraca numer tego licznika
// gdy licznik czasu upłynie 1000 ms, generuje zdarzenie, które jest umieszczane w kolejce środowiska uruchomieniowego
// gdy zdarzenie zostanie przetworzone przez środowisko uruchomieniowe, wykonywana jest funkcja (pierwszy parametr)
setTimeout(function () {
  // kod ten zostanie wykonany, gdy licznik czasu osiągnie wartość 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // wysyłane jest zdarzenie informujące, że wynik jest dostępny
  eventEmitter.emit("timer1Success", { success: 4 });
  // wysyłane jest kolejne zdarzenie informujące o dostępności kolejnego wyniku
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// subskrybuje się zdarzenie [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// subskrybuje się zdarzenie [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// zostanie wyświetlone przed komunikatami zdarzeń generowanymi przez funkcję powiązaną z [timer1]
console.log("[fin du code principal du script],", heure(débutScript));

// narzędzie do wyświetlania godziny i czasu trwania
function heure(début) {
  // aktualna godzina
  const now = moment(Date.now());
  // formatowanie godziny
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // czy należy obliczyć czas trwania?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatowanie godziny i czasu trwania
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // wynik
  return result;
}

Komentarze

  • w wierszu 9 importujemy klasę [EventEmitter] z biblioteki [events]. Jest to nowość: do tej pory importowaliśmy wyłącznie obiekty literałowe i funkcje;
  • w wierszu 15 tworzymy emiter zdarzeń [node.js] poprzez instancjonowanie klasy [EventEmitter] za pomocą słowa kluczowego [new];
  • wiersze 20–27: funkcja asynchroniczna [setTimeout]. Podczas wykonywania wygeneruje ona dwa zdarzenia:
    • w wierszu 24 – zdarzenie [timer1Success] z obiektem {success : 4} jako wartością powiązaną;
    • w wierszu 26 – zdarzenie [timer1Failure] z obiektem {failure: 6} jako wartością powiązaną;
    • funkcja asynchroniczna może generować dowolną liczbę zdarzeń. Wspomniano wcześniej, że najczęściej generuje jedno z dwóch zdarzeń [success, failure], a nie oba, jak ma to miejsce w tym przypadku;
  • wiersz 20: wykonanie zdarzenia [setTimeout] następuje natychmiast: uruchamiany jest licznik czasu, a jego numer jest zwracany do kodu wywołującego. Generowanie zdarzeń nastąpi później, w tym przypadku po upływie 1 sekundy;
  • wysyłanie zdarzeń jest bezcelowe, jeśli nie ma kodu, który mógłby je przetworzyć, gdy się pojawią. Dlatego kod główny musi zasubskrybować oba zdarzenia [timer1Success, timer1Failure], jeśli chce nimi zarządzać, a w szczególności pobrać dane związane z tymi zdarzeniami;
  • wiersze 30–32: kod główny subskrybuje zdarzenie [timer1Success]. Gdy punkt obsługi zdarzeń [node.js] przetworzy to zdarzenie, wywoła funkcję, która jest drugim parametrem metody [eventEmitter.on], przekazując jej dane (tutaj nazwane [result]) powiązane ze zdarzeniem [timer1Success];
  • wiersz 31: funkcja obsługująca zdarzenie wyświetli jSON danych powiązanych ze zdarzeniem wraz z aktualną godziną;
  • wiersze 35–37: za pomocą podobnego kodu kod główny subskrybuje zdarzenie [timer1Failure];
  • Subskrypcja zdarzenia (pierwszy parametr) nie powoduje natychmiastowego wykonania kodu funkcji [callback] (drugi parametr). Funkcja ta zostanie wykonana dopiero po wystąpieniu zdarzenia;
  • wiersz 40: główny kod skryptu został zakończony, ale sam skrypt nie, ponieważ główny kod uruchomił zadanie asynchroniczne. Cały skrypt zostanie zakończony dopiero po zakończeniu tego zadania asynchronicznego;

Wyniki to potwierdzają:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-02.js"
[début du script], heure=09:34:58:909
[fin du code principal du script], heure=09:34:58:916, durée= 0 seconde(s) et 10 millisecondes
[setTimeout, fin du timer d'1 s], heure=09:34:59:929, durée= 1 seconde(s) et 23 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=09:34:59:931, durée= 1 seconde(s) et 25 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=09:34:59:932, durée= 1 seconde(s) et 26 millisecondes, via l'événement [timer1Failure]

[Done] exited with code=0 in 1.627 seconds

  • wiersz 3: koniec głównego kodu 10 ms po rozpoczęciu skryptu;
  • wiersz 4: początek funkcji zamkniętej w timerze o czasie 1000 ms, około 1 sekundy po uruchomieniu skryptu;
  • wiersz 5: przetwarzanie zdarzenia [‘timer1Success’], 2 ms później;
  • wiersz 6: obsługa zdarzenia [‘timer1Failure’], 1 ms później niż zdarzenie [‘timer1Success’];
  • wiersz 8: zakończenie skryptu globalnego o łącznym czasie trwania 1,627 sekundy;

11.3. skrypt [async-03]

Poniższy skrypt pokazuje inny aspekt pętli zdarzeń [node.js]:

  • pętla wykonuje zdarzenia jedno po drugim, zazwyczaj w kolejności ich pojawiania się. Niektóre skrypty OS nadają priorytety zdarzeniom, które są następnie przetwarzane według priorytetu, a nie kolejności pojawiania się;
  • pętla wykonuje tylko jedno zdarzenie na raz. Kolejne zdarzenie jest przetwarzane dopiero po zakończeniu przetwarzania poprzedniego. W systemie zdarzeniowym należy zatem unikać pisania kodu, który przez dłuższy czas monopolizuje procesor, ponieważ wówczas zdarzenia nie są przetwarzane w momencie ich wystąpienia, ale dopiero później, gdy pętla zdarzeń do nich dotrze. W rezultacie otrzymujemy aplikację o niskiej „reaktywności”;

Skrypt [async-03] przedstawia przykład tego zjawiska:


'use strict';

// funkcje asynchroniczne mogą zwracać wynik poprzez wygenerowanie zdarzenia
// kod główny może pobrać te wyniki, subskrybując wysyłane zdarzenia

// importy
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// początek
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// emiter zdarzeń
const eventEmitter = new EventEmitter();

// setTimeout uruchamia licznik czasu o wartości 1000 ms (drugi parametr) i natychmiast zwraca numer tego licznika
// gdy licznik czasu upłynie 1000 ms, generuje zdarzenie, które jest umieszczane w kolejce środowiska uruchomieniowego
// gdy zdarzenie zostanie przetworzone przez środowisko uruchomieniowe, wykonywana jest funkcja (pierwszy parametr)
setTimeout(function () {
  // kod ten zostanie wykonany, gdy licznik czasu osiągnie wartość 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // wysyłane jest zdarzenie informujące, że wynik jest dostępny
  eventEmitter.emit("timer1Success", { success: 4 });
  // wysyłane jest kolejne zdarzenie informujące o dostępności kolejnego wyniku
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// subskrybuje się zdarzenie [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// subskrybuje się zdarzenie [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// nieco zasobochłonny kod synchroniczny, który uniemożliwił zakończenie działania kodu głównego przed zakończeniem [timer1]
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 10000; j++) {
    i + i ^ 2 + i ^ 3;
  }
}

// zostanie wyświetlony przed komunikatami zdarzeń wysyłanymi przez funkcję powiązaną z [timer1]
console.log("[fin du script],", heure(débutScript));

// narzędzie do wyświetlania godziny i czasu trwania
function heure(début) {
 ...
}

Komentarze

  • kod ten pochodzi z poprzedniego przykładu [async-02], do którego dodano wiersze 39–44;
  • wiersze 20–27: funkcja [setTimeout] została zaprogramowana tak, aby po upływie jednej sekundy wykonać wewnętrzną funkcję asynchroniczną. Po upływie tej sekundy wykonanie funkcji asynchronicznej timera nie następuje natychmiast: do pętli wykonawczej umieszczane jest zdarzenie, które ją wywołuje. Jeśli pętla wykonawcza jest zajęta przetwarzaniem innego zdarzenia, wykonanie funkcji asynchronicznej timera będzie musiało poczekać;
  • wiersze 20–27: gdy tylko funkcja [setTimeout] zbroi swój timer na czas trwania jednej sekundy, zwalnia procesor i oddaje kontrolę kodowi wywołującemu. Ten kontynuuje od wierszy 30–37, które są subskrypcjami zdarzeń i których czas wykonania jest znikomy;
  • główny kod przechodzi do wierszy 40–44, które tworzą pętlę o 1010 iteracjach. Kod ten będzie w trakcie wykonywania, gdy timer wygeneruje zdarzenie „koniec 1-sekundowego opóźnienia”. Zdarzenie to trafia wówczas do pętli zdarzeń, ale będzie musiało poczekać na zakończenie wykonywania głównego kodu skryptu, aby miało szansę zostać przetworzone;
  • wiersz 47: koniec głównego kodu skryptu. Dopiero po tym ostatnim wyświetleniu zdarzenie zakończenia timera będzie mogło zostać przetworzone, a wewnętrzna funkcja asynchroniczna [setTimeout] będzie mogła zostać wykonana;

Skrypt daje następujące wyniki:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-03.js"
[début du script], heure=08:55:02:665
[fin du code principal du script], heure=08:55:11:789, durée= 9 seconde(s) et 131 millisecondes
[setTimeout, fin du timer d'1 s], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Failure]

[Done] exited with code=0 in 9.796 seconds

Komentarze

  • wiersz 3: widać, że wykonanie głównego kodu skryptu zajęło 9 sekund. Zdarzenia, które mogły wystąpić w tym czasie, zostały umieszczone w kolejce w pętli zdarzeń;
  • wiersz 4: widać, że zdarzenie [fin du timer] zostało przetworzone 5 ms po zakończeniu kodu głównego. Zostało ono wygenerowane około 1 s po uruchomieniu skryptu, ale musiało czekać dodatkowe 8 s, zanim zostało ostatecznie przetworzone;

Z tego przykładu wynika, że w systemie opartym na zdarzeniach kod nigdy nie powinien zajmować procesora przez zbyt długi czas. Jeśli mamy kod synchroniczny, którego wykonanie trwa długo, musimy „poradzić sobie” z jego podziałem na krótsze zadania asynchroniczne, które zasygnalizują swoje zakończenie za pomocą zdarzenia.

11.4. skrypt [async-04]

Skrypt [async-04] ilustruje inny mechanizm, zwany [Promise], czyli obietnicę wyniku. Mechanizm ten pozwala uniknąć jawnego zarządzania zdarzeniami [node.js]. Odbywa się to niejawnie, dzięki czemu programista może zignorować istnienie tych zdarzeń. Zrozumienie ich pozwoli mu jednak lepiej pojąć działanie zdarzeń [Promise], które na pierwszy rzut oka wydają się skomplikowane.

Typ [Promise] jest klasą JavaScript. Jego konstruktor przyjmuje jako parametr funkcję asynchroniczną, do której przekazuje dwa parametry, tradycyjnie nazywane [resolve] i [reject]. Mogłyby one nosić inną nazwę;

1
2
3
4
5
6
7
const promise=new Promise(function(resolve, reject){
     // uruchamiane jest zadanie asynchroniczne
    
     // w przypadku powodzenia: wywołaj resolve(result), gdzie [result] jest wynikiem zadania asynchronicznego;
     // w przypadku niepowodzenia: wywołaj reject(error), gdzie [error] jest obiektem zawierającym napotkany błąd;
}
// subskrybować zdarzenia generowane przez zadanie asynchroniczne o identyfikatorze [Promise]
  • konstruktor klasy [Promise] wykonuje dwie czynności:
    • tworzy zdarzenie uruchamiające wykonanie funkcji [function(resolve, reject)], która została mu przekazana jako parametr, ale nie czeka na jej wynik i natychmiast zwraca obiekt [Promise] do kodu wywołującego. Obiekt ten może przybierać cztery stany:
      • [pending]: operacja asynchroniczna, która spowodowała powstanie [Promise], nie została jeszcze zakończona;
      • [fulfilled]: operacja asynchroniczna, która zwróciła [Promise], zakończyła się pomyślnie;
      • [rejected]: operacja asynchroniczna, która zwróciła [Promise], zakończyła się niepowodzeniem;
      • [settled]: operacja asynchroniczna, która zwróciła [Promise], została zakończona;

Gdy konstruktor zwraca wynik, utworzony obiekt [Promise] znajduje się w stanie [pending], oczekując na wyniki funkcji asynchronicznej;

  • (ciąg dalszy)
    • zadanie asynchroniczne z wierszy 2–5 jest uruchamiane natychmiast. Zadania asynchroniczne to najczęściej zadania wejścia/wyjścia, które można podzielić w następujący sposób:
      1. wykonanie kodu synchronicznego w celu uruchomienia operacji wejścia/wyjścia z innym podmiotem, na przykład serwerem zdalnym;
      2. oczekiwanie na odpowiedź tego podmiotu;
      3. przetwarzanie tej odpowiedzi;

To właśnie faza 2, czyli oczekiwanie na odpowiedź podmiotu zewnętrznego względem procesora, jest najbardziej kosztowna. Zamiast czekać:

  • odbiór żądanych danych z urządzenia zewnętrznego zostanie zasygnalizowany przez zdarzenie;
  • w kodzie synchronicznym, który nastąpi po fazie 1 (wiersz 7 przykładowego kodu), zasubskrybujemy to zdarzenie, a następnie w pewnym momencie powrócimy do pętli zdarzeń [node.js]. Zostanie wówczas przetworzone kolejne zdarzenie z listy oczekujących zdarzeń;
  • w fazie 2 występuje równoległość wykonywania, ale na różnych urządzeniach:
    • procesor dla pętli zdarzeń;
    • urządzenie zewnętrzne (dysk, baza danych, serwer zdalny) – w przypadku wyszukiwania żądanych danych;
  • pod koniec fazy 2, gdy operacja wejścia/wyjścia uzyskała żądane dane, zostanie wygenerowane zdarzenie sygnalizujące, że wynik operacji wejścia/wyjścia jest dostępny. Zdarzenie to dołączy wówczas do pozostałych na liście zdarzeń oczekujących;
  • gdy nadejdzie jego kolej, zostanie przetworzone. Funkcja powiązana z tym zdarzeniem (wiersz 7 przykładowego kodu) zostanie wówczas wykonana;

Taki sposób działania pozwala uniknąć przestojów: sytuacji, w której procesor czeka na odpowiedź od urządzenia wolniejszego od siebie;

  • (ciąg dalszy)
    • gdy zadanie asynchroniczne z wierszy 2 i 5 zostało uruchomione i zakończyło swoją pracę, ma ono możliwość zwrócenia wyniku do kodu wywołującego dzięki dwóm funkcjom [resolve, reject], które konstruktor [Promise] przekazał mu jako parametry. Obowiązuje następująca konwencja:
      • zadanie asynchroniczne sygnalizuje powodzenie za pomocą funkcji [resolve(result)]. Sprowadza się to do umieszczenia w pętli zdarzeń funkcji [node.js] zdarzenia, które można by nazwać [resolved], z [result] jako powiązanym danymi;
      • zadanie asynchroniczne zgłasza niepowodzenie za pomocą kodu [reject(error)]. Sprowadza się to do umieszczenia w pętli zdarzeń [node.js] zdarzenia, które można by nazwać [rejected], z [error] jako powiązanymi danymi – zazwyczaj jest to obiekt zawierający szczegółowe informacje o wystąpionym błędzie;
      • dlatego kod wywołujący musi zasubskrybować te dwa zdarzenia, aby otrzymać powiadomienie o dostępności wyniku funkcji asynchronicznej;

Po zakończeniu wykonywania zadania asynchronicznego zawartego w [Promise] zmienia się stan obiektu [promise] zwróconego przez konstruktor [Promise(…)]:

  • zdarzenie [resolved] powoduje przejście obiektu ze stanu [pending] do stanu [resolved];
  • zdarzenie [rejected] powoduje przejście obiektu ze stanu [pending] do stanu [rejected];

Subskrypcja zdarzeń [resolved] i [rejected] zadania asynchronicznego odbywa się za pomocą metod klasy [Promise] przy użyciu następującej składni:

promise.then(f1).catch(f2).finally(f3);

gdzie:

  • f1 to funkcja uruchamiana, gdy stan [promise] zmienia się z [pending] na [resolved], czyli gdy zadanie asynchroniczne zakończyło się powodzeniem. Jako parametr otrzymuje ona wartość [result], przekazaną przez instrukcję [resolve(result)] zadania asynchronicznego;
  • f2 to funkcja wykonywana, gdy stan [promise] zmienia się z [pending] na [rejected], czyli gdy zadanie asynchroniczne nie wykonało swojego zadania. Jako parametr otrzymuje ona wartość [error], przekazaną przez instrukcję [reject(errror)] zadania asynchronicznego;
  • f3 to funkcja wykonywana po wykonaniu metod [then] lub [catch], a zatem wykonywana za każdym razem. Nie otrzymuje ona żadnych parametrów;

Ta składnia całkowicie ukrywa zdarzenia, na które subskrybujemy. Jest to jednak subskrypcja i podobnie jak w poprzednim przykładzie nie powoduje ona natychmiastowego wykonania funkcji [f1, f2, f3]. Funkcje te zostaną wykonane lub nie, gdy wystąpi jedno ze zdarzeń [resolved, rejected], na które subskrybujemy.

Skrypt [async-04] ilustruje ten mechanizm:


'use strict';

// można uzyskać wyniki (succes, failure) funkcji asynchronicznej
// bez wyraźnego korzystania z zdarzeń dzięki klasie [Promise]
// klasa ta wykorzystuje zdarzenia w sposób domyślny, ale nie są one widoczne w kodzie

// importy
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// początek
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));

// definicja zadania asynchronicznego przy użyciu obietnicy [Promise]
// zadanie asynchroniczne jest parametrem konstruktora [Promise]
const débutPromise1 = moment(Date.now());
const promise1 = new Promise(function (resolve) {
  // log
  console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
  // kod asynchroniczny
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
    // zadanie asynchroniczne zwraca wynik za pomocą funkcji [resolve]
    // obietnica zostaje wówczas zrealizowana
    resolve('[réussite]');
  }, 1000)
});

// można poznać wynik obietnicy [promise1]
// gdy obietnica została zrealizowana (resolve) lub odrzucona (reject)
// kolejna instrukcja to subskrypcja zdarzenia [resolved] za pomocą metody [then]
// oraz na zdarzenie [rejected] za pomocą metody [catch]
// metoda [finally] jest wykonywana zarówno po instrukcji „then”, jak i „catch”
promise1.then(result => {
  // w przypadku pomyślnego wykonania obietnicy  [evt resolved]
  console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // przypadek błędu  [evt rejected]
  console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // wykonane w każdym przypadku
  console.log("[promise1.finally]", heure(débutPromise1));
});

// zdefiniowanie zadania asynchronicznego za pomocą obietnicy [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise(function (resolve, reject) {
  // log
  console.log("[début fonction asynchrone de promise2],", heure(débutPromise1));
  // zadanie asynchroniczne
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
    // zadanie asynchroniczne zwraca wynik za pomocą funkcji [reject]
    // obietnica zostaje wówczas złamana
    reject('[échec]');
  }, 2000)
});

// można poznać wynik obietnicy [promise2]
// gdy została ona rozwiązana (resolve) lub odrzucona (reject)
promise2.then(result => {
  // w przypadku powodzenia obietnicy [evt resolved]
  console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // w przypadku błędu [evt rejected]
  console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // wykonane w każdym przypadku
  console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});

// zostanie wyświetlone przed komunikatami funkcji asynchronicznych i komunikatami powiązanych zdarzeń
console.log("[fin du code principal du script],", heure(débutScript));

// narzędzie
function heure(début) {
  // aktualny czas
  const now = moment(Date.now());
  // formatowanie godziny
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatowanie czasu trwania
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // wynik
  return result;
}

Komentarze

  • wiersze 18–28: utworzenie zdarzenia [Promise promise1]. Jego funkcja asynchroniczna zwraca wynik za pośrednictwem zdarzenia po upływie jednej sekundy. Po uruchomieniu tej operacji asynchronicznej (uruchomienie timera) nie czekamy na zwrot wyniku i od razu przechodzimy do kodu w wierszu 35;
  • wiersze 35–44: subskrybujemy dwa zdarzenia [resolved, rejected], które może wygenerować wewnętrzna funkcja asynchroniczna [promise1];
  • wiersze 46–71: powtarzamy tę samą sekwencję kodu, co poprzednio, dla drugiej obietnicy [promise2];
  • wiersz 74: główny kod skryptu został zakończony, ale nie cały skrypt, ponieważ uruchomiono dwie operacje asynchroniczne. Powracamy do pętli zdarzeń, gdzie w pewnym momencie wystąpi jedno ze zdarzeń [resolved, rejected] związanych z obietnicami [promise1, promise2]. Zostanie ono wówczas przetworzone;
  • następnie nastąpi powrót do pętli zdarzeń. W tym momencie drugie zdarzenie [resolved, rejected] z obietnic [promise1, promise2] zostanie przetworzone, gdy tylko wystąpi;

Wykonanie


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-04.js"
[début du script], heure=09:39:05:950, durée= 0 seconde(s) et 3 millisecondes
[début fonction asynchrone de promise1], heure=09:39:05:958, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=09:39:05:959, durée= 0 seconde(s) et 1 millisecondes
[fin du code principal du script], heure=09:39:05:960, durée= 0 seconde(s) et 13 millisecondes
[fin fonction asynchrone de promise1], heure=09:39:06:977, durée= 1 seconde(s) et 19 millisecondes
[promise1.then], heure=09:39:06:980, durée= 1 seconde(s) et 21 millisecondes, result=[réussite]
[promise1.finally] heure=09:39:06:982, durée= 1 seconde(s) et 24 millisecondes
[fin fonction asynchrone de promise2], heure=09:39:07:976, durée= 2 seconde(s) et 17 millisecondes
[promise2.catch], heure=09:39:07:978, durée= 2 seconde(s) et 19 millisecondes, result=[échec]
[promise2.finally], heure=09:39:07:980, durée= 2 seconde(s) et 21 millisecondes

[Done] exited with code=0 in 2.589 seconds

Komentarze

  • wiersz 3: uruchamiana jest funkcja asynchroniczna [promise1], ale nie oczekuje się na jej zakończenie, które zostanie zasygnalizowane przez zdarzenie;
  • wiersz 4: uruchamiana jest funkcja asynchroniczna [promise2], ale nie oczekuje się na jej zakończenie, które zostanie zasygnalizowane przez zdarzenie;
  • wiersz 5: koniec kodu głównego i powrót do pętli zdarzeń;
  • wiersz 6: przetwarzanie zdarzenia [fin fonction asynchrone de promise1]. Stan [promise1] zmieni się na [resolved]. Zostanie to zasygnalizowane przez zdarzenie;
  • wiersz 7: ponieważ [promise2] nadal nie zakończył swojej pracy, zdarzenie [promise1 resolved], które właśnie zostało umieszczone w pętli, zostanie przetworzone przez metodę [promise1.then], a następnie przez metodę [promise.finally] (wiersz 8);
  • wiersze 9–11: ten sam mechanizm ma miejsce, gdy [promise2] przechodzi ze stanu [pending] do stanu [resolved];

11.5. skrypt [async-05]

Wróćmy do kodu konstruktora obiektu [Promise]:

1
2
3
4
5
6
7
8
9
'use strict'; 

const promise=new Promise(function(resolve, reject){
// uruchomiono zadanie asynchroniczne
// …
// jeśli zakończy się powodzeniem: wywołaj resolve(result), gdzie [result] jest wynikiem zadania asynchronicznego;
// w przypadku niepowodzenia: wywołaj reject(error), gdzie [error] jest obiektem zawierającym napotkany błąd;
}
// subskrybujemy zdarzenia generowane przez zadanie asynchroniczne

W wierszu 2 uruchamiane jest zadanie asynchroniczne obiektu [Promise]. Często wymaga ono większej liczby parametrów niż tylko te, które przekazuje mu funkcja, która je enkapsuluje ([resolve, reject]). W takim przypadku tworzenie obiektu [Promise] zamykamy w funkcji, która przekaże mu parametry potrzebne do działania jego funkcji asynchronicznej:

'use strict';

// definicja funkcji asynchronicznej
function uneFonctionAsynchrone (p1, p2, , pn){
 return new Promise(function(resolve, reject){
     // uruchamiane jest zadanie asynchroniczne z parametrami (P1, p2, …, pn)
    // …
     // w przypadku powodzenia: wywołaj resolve(result), gdzie [result] jest wynikiem zadania asynchronicznego;
     // w przypadku niepowodzenia: wywołaj reject(error), gdzie [error] jest obiektem zawierającym napotkany błąd;
}
// subskrybujemysubskrybuje zdarzenia [resolved, rejected], które będzie generować funkcja asynchroniczna [uneFonctionAsynchrone]

// chwilę później wywoływana jest funkcja asynchroniczna [uneFonctionAsynchrone]
uneFonctionAsynchrone(e1, e2, , en) ;

Poniższy skrypt:

  • definiuje dwie funkcje asynchroniczne zwracające [Promise];
  • uruchamia ich równoległe wykonywanie i czeka, aż obie się zakończą, aby wykonać określoną czynność;

'use strict';

// można definiować funkcje asynchroniczne, które zwracają typ [Promise]
// można je wówczas oznaczyć słowem kluczowym [async]

// importy
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// początek
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// funkcja asynchroniczna, która zwraca obietnicę [Promise]
function async01(p1) {
  return new Promise((resolve) => {
    console.log("[début de la tâche asynchrone async01]");
    // zadanie asynchroniczne
    const débutAsync01 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
      // zadanie asynchroniczne może zwrócić złożony wynik
      resolve({
        prop1: [10, 20, 30],
        prop2: "abcd",
        prop3: p1,
      });
    }, 1000)
  });
}

// funkcja asynchroniczna, która zwraca obietnicę [Promise]
function async02(p1, p2) {
  return new Promise(resolve => {
    console.log("[début de la tâche asynchrone async02]");
    // zadanie asynchroniczne
    const débutAsync02 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
      // zadanie asynchroniczne może zwrócić złożony wynik
      resolve({
        prop1: [11, 21, 31],
        prop2: "xyzt",
        prop3: p1 + p2
      });
    }, 2000)
  })
}

// uruchamia się obie funkcje asynchroniczne równolegle
// i czeka się, aż obie się zakończą
// instrukcja „then” zostanie wykonana tylko wtedy, gdy obie funkcje wygenerowały zdarzenie [resolved]
// blok catch zostanie wykonany, gdy tylko jedna z tych dwóch funkcji wygeneruje zdarzenie [rejected]
Promise.all([async01(10), async02(10, 20)])
  // wynikiem jest tablica [result1, result2], gdzie [result1] jest wynikiem wygenerowanym przez [resolve] z [async01]
  //, a [result2] to wynik wygenerowany przez [resolve] z [async02]
  .then(result => {
    console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
  })
  // error to wynik zwrócony przez pierwszy [reject] jednej z dwóch funkcji asynchronicznych
  .catch(error => {
    console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
  })
  // finally jest wykonywane po then lub catch
  .finally(() => {
    console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
  });

// zostanie wyświetlone przed komunikatami funkcji asynchronicznych i powiązanych zdarzeń
console.log("[fin du code principal du script],", heure(débutScript));

// narzędzie
function heure(début) {
  // aktualny czas
  const now = moment(Date.now());
  // formatowanie godziny
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatowanie czasu trwania
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // wynik
  return result;
}

Komentarze

  • wiersze 15–30: definiujemy funkcję [async01], która zwraca wynik po upływie 1 sekundy za pośrednictwem zdarzenia timera. Funkcja [async01], której wynik jest wykorzystywany w wierszu 26;
  • wiersze 33–47: podobnie postępujemy z funkcją [async02], która zwraca wynik po 2 sekundach za pomocą zdarzenia timera. Funkcja [async02] przyjmuje dwa parametry, które są wykorzystywane w jej wyniku w wierszu 44;
  • gdy zostaną wywołane obie funkcje [async01, async02]:
    • zostaną uruchomione;
    • zwrócą kodowi wywołującemu dwie obietnice [promise1, promise2];
    • następnie wykonanie powróci do kodu wywołującego, który będzie kontynuował działanie;
    • po około 1 sekundzie funkcja [async01] wygeneruje zdarzenie informujące o zakończeniu pracy. Zdarzenie to zostanie umieszczone w kolejce zdarzeń powiązanej z wynikiem przekazanym przez funkcję [async01] wraz ze zdarzeniem;
    • po około 2 sekundach ten sam proces nastąpi w przypadku [async02];
  • wiersz 54: dopiero teraz wykonywane są funkcje asynchroniczne [async01, async02] (oznaczenia async01(10) i async02(10,20)). Są one wykonywane w tablicy przekazanej jako parametr do metody [Promise.all]. Wiadomo, że obie metody [async01, async02] zwracają obietnicę do kodu wywołującego. Zatem parametrem metody [Promise.all] jest tablica zawierająca dwie obietnice;
  • [Promise.all([promise1, promise2, …, promisen]).then(f1).catch(f2).finally(f3)] jest subskrypcją zdarzeń:
    • [Promise.all] jest typu [Promise];
    • funkcja [f1] metody [then] zostanie wykonana, gdy wszystkie obietnice [promise1, promise2, …, promisen] z tablicy parametrów metody [all] przejdą zestanu [pending] do stanu [resolved]. Innymi słowy, funkcja [f1] zostanie wykonana, gdy wszystkie obietnice z tablicy zakończą się pomyślnie;
    • funkcja [f2] metody [catch] zostanie wykonana, gdy tylko jedna z obietnic z tablicy przejdzie ze stanu [pending] do stanu [rejected]. Innymi słowy, funkcja [f2] zostanie wykonana, gdy tylko jedna z obietnic z tablicy zakończy się niepowodzeniem;
    • funkcja [f3] metody [finally] zostanie wykonana po wykonaniu jednej z metod [then, catch], a zatem zawsze zostanie wykonana;

Wykonanie kodu daje następujące wyniki:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-05.js"
[début du script], heure=12:17:17:367
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=12:17:17:375, durée= 0 seconde(s) et 10 millisecondes
[fin de la tâche asynchrone async01], heure=12:17:18:391, durée= 1 seconde(s) et 17 millisecondes
[fin de la tâche asynchrone async02], heure=12:17:19:389, durée= 2 seconde(s) et 14 millisecondes
[promise-all success], heure=12:17:19:390, durée= 2 seconde(s) et 25 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=12:17:19:392, durée= 2 seconde(s) et 27 millisecondes

[Done] exited with code=0 in 2.572 seconds
  • wiersze 6–7: uruchomiono dwa zadania asynchroniczne [async01, async02]. Działają one równolegle. Nie chodzi o to, że ich kod jest wykonywany równolegle, ale o to, że ich oczekiwanie na żądane dane przebiega w tym samym czasie;
  • wiersz 5: główny kod skryptu został zakończony. Pozostaje czekać na zakończenie obu zadań asynchronicznych [async01, async02];
  • wiersz 6: zadanie asynchroniczne [async01] kończy się około 1 s po uruchomieniu. Zwraca wynik za pomocą funkcji [resolve], więc jej obietnica w tablicy w wierszu 56 kodu przechodzi ze stanu [pending] do stanu [resolved]. Nie wystarcza to jednak do wywołania metody [then], wiersze 59–60 kodu;
  • wiersz 7: zadanie asynchroniczne [async02] kończy się około 2 s po uruchomieniu. Zwraca wynik za pomocą funkcji [resolve], więc jej obietnica w tablicy w wierszu 56 kodu przechodzi ze stanu [pending] do stanu [resolved]. Metoda [then] zostanie wykonana, gdy tylko pozwoli na to pętla zdarzeń;
  • wiersz 8: wykonywana jest metoda [then] z [Promise.all]. Otrzymuje ona jako parametr tablicę [result1, result2], gdzie [result1] jest wynikiem wygenerowanym przez [async01], a [result2] jest wynikiem wygenerowanym przez [async02];
  • wiersz 9: wykonywana jest metoda [finally] z [Promise.all];

11.6. skrypt [async-06]

Ten nowy skrypt pokazuje, jak wspólne użycie słów kluczowych [async / await] pozwala uzyskać kod asynchroniczny przypominający kod synchroniczny. Obsługa zdarzeń jest całkowicie ukryta, co ułatwia zrozumienie kodu.

Wracamy do poprzedniego przykładu, wprowadzając w nim następujące zmiany:

  • dodajemy trzecią funkcję asynchroniczną [async03], która zwraca swój wynik za pomocą metody [Promise.reject], co sygnalizuje pętli zdarzeń, że „nie udało jej się” wykonać zadania;
  • wykonujemy sekwencyjnie trzy funkcje asynchroniczne [async01, async02, async03]. W poprzednim przykładzie funkcje asynchroniczne [async01, async02] były wykonywane równolegle;
  • przed pojawieniem się słów kluczowych [async/await] sekwencyjne wykonywanie akcji asynchronicznych odbywało się za pomocą zagnieżdżonych w sobie funkcji [Promise]. Gdy tylko pojawiało się kilka akcji asynchronicznych do wykonania w ten sposób, liczba obietnic odpowiednio wzrastała, a kod stawał się mniej czytelny;
  • dzięki słowom kluczowym [async/await] sekwencyjne wykonywanie zadań asynchronicznych odbywa się przy użyciu składni podobnej do tej stosowanej przy wykonywaniu zadań synchronicznych:

// funkcja asynchroniczna – użycie async / await
async function main() {
  // sekwencyjne wykonywanie zadań asynchronicznych
  try {
    // wykonanie z oczekiwaniem na [async01]
    const result1 = await async01(...);
    console.log("[async01 result]=", result1);
    // wykonanie z oczekiwaniem na [async02]
    const result2 = await async02(...);
    console.log("[async02 result]=", result2);
    // wykonanie z oczekiwaniem na [async03]
    const result3 = await async03(...);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // jedna z operacji asynchronicznych zakończyła się niepowodzeniem
    console.log(sprintf("[sequential error]= %j, %s", error));
  } finally {
    // zakończono
    console.log("[fin exécution séquentielle des tâches asynchrones],");
  }
  • wiersz 6: uruchamiana jest funkcja asynchroniczna [async01] (słowo kluczowe await) i oczekuje się, aż opublikuje ona swój wynik za pomocą jednej z metod [Promise.resolve, Promise.reject]. Jest to zatem operacja blokująca;
  • wiersz 6: słowo kluczowe [await] przekształca operację asynchroniczną [async01] w operację blokującą. Wiemy, że operacja [async01] zwraca wynik na dwa sposoby:
    • prawie natychmiast zwraca kodowi wywołującemu obiekt [Promise];
    • później publikuje wynik w pętli zdarzeń za pomocą metod [Promise.resolve, Promise.reject]. To właśnie ten drugi wynik pobiera [result1] w wierszu 6. Obsługa zdarzeń akcji [async01] stała się niewidoczna;
    • jeśli wynik [result] z [async01] zostanie opublikowany przez [Promise.resolve(result)], zostanie on przypisany do [result1] w wierszu 6, a wykonanie będzie kontynuowane w wierszu 7;
    • jeśli wynik funkcji [async01] jest publikowany przez funkcję [Promise.reject], powoduje to wygenerowanie wyjątku, a wykonanie kodu przechodzi do linii 14, czyli do bloku catch. Parametrem klauzuli [catch] jest obiekt błędu (error) opublikowany przez [async01] wraz z wyrażeniem [Promise.reject(error)]. Zadanie asynchroniczne może również zgłosić błąd za pomocą [throw(error)]. Obiekt [error] jest tym samym obiektem, który został pobrany w [catch(error)];
    • słowo kluczowe [await] musi obowiązkowo znajdować się w funkcji poprzedzonej słowem kluczowym [async] w wierszu 2. Słowo to wskazuje, że funkcja [main] jest funkcją asynchroniczną;
    • w wyrażeniu [await f(…)] funkcja [f] musi być funkcją asynchroniczną zwracającą obiekt [Promise] do kodu wywołującego;
  • to samo powtarzamy dla akcji asynchronicznej [async02] w wierszu 9 oraz [async03] w wierszu 12;

Wciąż korzystając ze słów kluczowych [async / await], można wykonać równoległą realizację zadań asynchronicznych przy użyciu następującej składni:


try {
    // równoległe wykonywanie zadań asynchronicznych
    const result = await Promise.all([async01(...), async02(...), async03(...)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // jedna z operacji asynchronicznych zakończyła się niepowodzeniem
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // zakończono
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
  • wiersz 3: mamy operację blokującą: czekamy, aż trzy zadania asynchroniczne z tablicy [async01(..), async02(..), async03(..)] opublikują swoje wyniki w pętli zdarzeń za pomocą jednej z metod [Promise.resolve, Promise.reject];
  • jeśli wszystkie trzy zadania asynchroniczne opublikują swoje wyniki za pomocą [Promise.resolve], wówczas stała [result] jest tablicą [result1, result2, result3], gdzie:
    • [result1] jest wynikiem opublikowanym przez [async01] przy użyciu wyrażenia [Promise.resolve(result1)];
    • [result2] jest wynikiem opublikowanym przez [async02] przy użyciu wyrażenia [Promise.resolve(result2)];
    • [result3] to wynik opublikowany przez [async03] przy użyciu wyrażenia [Promise.resolve(result3)];
  • jeśli którekolwiek z trzech zadań opublikuje swój wynik z wyrażeniem [Promise.reject(error)], nastąpi wyjątek;
    • stała [result] z linii 3 nie otrzymuje swojej wartości;
    • wykonanie przechodzi bezpośrednio do wyrażenia [catch] w wierszu 5;
    • parametr (error) bloku catch jest obiektem (error) opublikowanym przez wyrażenie [Promise.reject(error)];

Łącząc te dwie składnie, można wykonywać zadania asynchroniczne zarówno sekwencyjnie, jak i równolegle, a wszystko to przy użyciu składni analogicznej do kodu synchronicznego. Należy zatem preferować tę składnię, która jest znacznie bardziej czytelna niż poprzednie. Ta składnia [async / await] jest dostępna dopiero od wersji 6 programu ECMAScript. Nadal istnieje wiele kodów JavaScript wykorzystujących obietnice [Promise]. Dlatego ważne jest, aby zrozumieć również sposób ich działania.

Pełny kod skryptu [async-06] wygląda następująco:


'use strict';

// równoległe lub sekwencyjne wykonywanie wielu zadań asynchronicznych
// z użyciem słów kluczowych async / await

// importy
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// początek
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());

// funkcja asynchroniczna zwracająca [Promise]
function async01(débutAsync01) {
  return new Promise(function (resolve) {
    console.log("[début fonction asynchrone async01],", heure());
    // funkcja asynchroniczna
    setTimeout(function () {
      console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
      // operacja asynchroniczna może zwrócić złożony wynik
      // tutaj sukces
      resolve({
        prop1: [11, 21, 31],
        prop2: "abcd"
      });
    }, 1000)
  });
}

// funkcja asynchroniczna zwracająca [Promise]
function async02(débutAsync02) {
  console.log("[début fonction asynchrone async02],", heure());
  return new Promise(function (resolve) {
    // funkcja asynchroniczna
    setTimeout(function () {
      console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
      // operacja asynchroniczna może zwrócić złożony wynik
      // tutaj sukces
      resolve({
        prop1: [12, 22, 32],
        prop2: "xyzt"
      });
    }, 2000)
  })
}

// funkcja asynchroniczna zwracająca [Promise]
function async03(débutAsync03) {
  console.log("[début fonction asynchrone async03],", heure());
  return new Promise((resolve, reject) => {
    // funkcja asynchroniczna
    setTimeout(function () {
      console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
      // operacja asynchroniczna może zwrócić złożony wynik
      // tutaj wystąpiła awaria
      reject({
        prop1: [13, 23, 33],
        prop2: "échec"
      });
    }, 3000)
  })
}

// funkcja asynchroniczna – użycie async / await
async function main() {
  const débutSequential = moment(Date.now());
  // sekwencyjne wykonywanie zadań asynchronicznych
  console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
  try {
    // wykonanie z oczekiwaniem na [async01]
    const débutAsync01 = moment(Date.now());
    const result1 = await async01(débutAsync01);
    console.log("[async01 result]=", result1);
    // wykonanie z oczekiwaniem na [async02]
    const débutAsync02 = moment(Date.now());
    console.log("début async02-------------", heure());
    const result2 = await async02(débutAsync02);
    console.log("[async02 result]=", result2);
    // wykonanie z oczekiwaniem na [async03]
    const débutAsync03 = moment(Date.now());
    console.log("début async03-------------", heure());
    const result3 = await async03(débutAsync03);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // jedna z operacji asynchronicznych zakończyła się niepowodzeniem
    console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
  } finally {
    // zakończono
    console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
  }

  const débutParallel = moment(Date.now());
  // równoległe wykonywanie zadań asynchronicznych
  console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------")
  try {
    const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // jedna z operacji asynchronicznych zakończyła się niepowodzeniem
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // zakończono
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
  }

  // zakończono
  console.log("[fin de la fonction main],", heure(débutSequential));
}
// wykonanie funkcji asynchronicznej main
main();

// zostanie wyświetlony przed poszczególnymi komunikatami dotyczącymi funkcji asynchronicznych i ich zdarzeń
console.log("[fin du code principal du script],", heure(débutScript));

// narzędzie
function heure(début) {
  // aktualny czas
  const now = moment(Date.now());
  // formatowanie godziny
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatowanie czasu trwania
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // wynik
  return result;
}

Wyniki wykonania są następujące:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-06.js"
[début du code principal du script], heure=15:02:00:152
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:00:161
[fin du code principal du script], heure=15:02:00:164, durée= 0 seconde(s) et 15 millisecondes
[fin fonction asynchrone async01], heure=15:02:01:165, durée= 1 seconde(s) et 4 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=15:02:01:253
[début fonction asynchrone async02], heure=15:02:01:254
[fin fonction asynchrone async02], heure=15:02:03:265, durée= 2 seconde(s) et 12 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=15:02:03:268
[début fonction asynchrone async03], heure=15:02:03:268
[fin fonction asynchrone async03], heure=15:02:06:285, durée= 3 seconde(s) et 18 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=15:02:06:289, durée= 6 seconde(s) et 129 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=15:02:06:291, durée= 6 seconde(s) et 131 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:06:292
[début fonction asynchrone async02], heure=15:02:06:293
[début fonction asynchrone async03], heure=15:02:06:294
[fin fonction asynchrone async01], heure=15:02:07:294, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=15:02:08:298, durée= 2 seconde(s) et 6 millisecondes
[fin fonction asynchrone async03], heure=15:02:09:297, durée= 3 seconde(s) et 5 millisecondes
[parallel error], heure=15:02:09:298, durée= 3 seconde(s) et 6 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=15:02:09:299, durée= 3 seconde(s) et 7 millisecondes
[fin de la fonction main], heure=15:02:09:300, durée= 9 seconde(s) et 140 millisecondes

[Done] exited with code=0 in 9.668 seconds