11. Programowanie zdarzeniowe i funkcje asynchroniczne

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]:

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ę;
- 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;
- 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:
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:
- wykonanie kodu synchronicznego w celu uruchomienia operacji wejścia/wyjścia z innym podmiotem, na przykład serwerem zdalnym;
- oczekiwanie na odpowiedź tego podmiotu;
- przetwarzanie tej odpowiedzi;
- 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:
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;
- 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:
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]:
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:
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