Optymalizacja zadań asynchronicznych w JavaScript i React Native - Promise.all()

Nie tak dawno opisywałem metodę Future.wait() języka Dart (wpis znajduje się tutaj), dzięki której możemy zoptymalizować działania asynchroniczne. Oczywiście JavaScript niczym nie ustępuje Dartowi i aplikacje pisane w React Native również mogą wykorzystywać tego rodzaju funkcję, która zwiększy prędkość działania aplikacji z kilkoma metodami, które potrzebują czasu na ich wykonanie.

Opiszę tutaj dokładnie ten sam przykład, co poprzednim razem. Zacznijmy więc od przykładu pokazującego nieoptymalne pobieranie danych z API, gdzie żądania wykonywane są jedno po drugim:

let responseJavaScript =
  await fetch('https://www.googleapis.com/books/v1/volumes?q={javascript}');
let responseReactNative =
  await fetch('https://www.googleapis.com/books/v1/volumes?q={reactnative}');
let responseAndroid =
  await fetch('https://www.googleapis.com/books/v1/volumes?q={android}');
let responseIos =
  await fetch('https://www.googleapis.com/books/v1/volumes?q={ios}');

Widzimy tutaj kolejne żądania API odnośnie książek, każde dotyczące innego hasła. To, co powoduje, że powyższy kod nie jest optymalny, wynika z tego, że za każdym razem, aby wykonać kolejne żądanie, musimy odczekać aż poprzednie się wykona, chociaż w rzeczywistości nie potrzebujemy znać wcześniejszej odpowiedzi, aby wykonać kolejne odpytanie API.

Przyjmijmy w tym przypadku, że:

  • pierwsze żądanie wykona się w czasie 200 ms
  • drugie żądanie wykona się w czasie 150 ms
  • trzecie żądanie wykona się w czasie 300 ms
  • czwarte żądanie wykona się w czasie 250 ms

W tym przykładzie, aby obliczyć czas potrzebny na wykonanie tych wszystkich żądań, wystarczy wszystkie te czasy ze sobą zsumować. Znaczy to, że zostaną one wykonane w czasie 900 ms.

Więc jak to zoptymalizować? Z pomocą tutaj przychodzi funkcja Promise.all(), której zadaniem jest na poczekanie na wykonanie się wszystkich zadań asynchronicznych, oraz zwrócenie listy zawierającej wyniki tych działań. Spróbujmy zobaczyć powyższy przykład napisany z wykorzystaniem Promise.all():

let responses = await Promise.all([
  fetch('https://www.googleapis.com/books/v1/volumes?q={javascript}'),
  fetch('https://www.googleapis.com/books/v1/volumes?q={reactnative}'),
  fetch('https://www.googleapis.com/books/v1/volumes?q={android}'),
  fetch('https://www.googleapis.com/books/v1/volumes?q={ios}')
]);

let responseJavaScript = responses[0];
let responseReactNative = responses[1];
let responseAndroid = responses[2];
let responseIos = responses[3];

Biorąc pod uwagę wyżej wymienione czasy wykonywania się każdego z żądań, wykorzystanie Promise.all() sprawi, że wszystkie te żądania wykonają się w ciągu 300 ms. Ten czas tak naprawdę wynika z tego, że dokładnie tyle wynosi najdłuższy czas każdego z tych żądań.

Promise.all() przyjął listę zadań asynchronicznych, które zostały uruchomione w jednym momencie i oczekuje, aż wszystkie z nich zakończą się sukcesem. W tym przypadku wynikiem jest lista odpowiedzi w kolejności takiej, w jakiej żądania zostały przekazane, a nie w kolejności od najszybciej wykonanego.

Wiadome jest to, że w przypadku wykonywania każdego z tych działań może pojawić się błąd. W przypadku wystąpienia pierwszego błędu pozostałe zadania są anulowane oraz zostaje rzucony wyjątek.

Jednak co zrobić w przypadku, kiedy chcemy znać odpowiedzi żądań, które zakończyły się powodzeniem, a tylko ominąć te, w przypadku których pojawił się błąd? To również jest bardzo łatwe zadanie i wystarczy złapać wyjątek w wykonywanych zadaniach:

let responses = await Promise.all([
  fetch('https://www.googleapis.com/books/v1/volumes?q={javascript}')
    .catch(() => { }),
  fetch('https://www.googleapis.com/books/v1/volumes?q={reactnative}')
    .catch(() => { }),
  fetch('https://www.googleapis.com/books/v1/volumes?q={android}')
    .catch(() => { }),
  fetch('https://www.googleapis.com/books/v1/volumes?q={ios}')
    .catch(() => { })
]);

let responseJavaScript = responses[0];
let responseReactNative = responses[1];
let responseAndroid = responses[2];
let responseIos = responses[3];

Jeśli w powyższym przykładzie, żądanie do API o wyniki dotyczące systemu iOS spowoduje wystąpienie błędu, zmienna responseIos będzie miała wartość undefined, a te, które zostały wykonane prawidłowo, będą posiadały prawidłowe odpowiedzi.

Jak widać, przy pomocy Promise.all() możemy w znacznym stopniu przyspieszyć wykonywanie działań asynchronicznych, sprawiając, że zostaną one wystartowane w jednej chwili, a czas wykonywania wszystkich zadań, będzie dokładnie równy czasowi najdłuższego z tych zadań. Ta sztuczka zadziała jednak tylko w przypadku, gdy wynik wcześniejszego żądania nie jest przekazywany do któregoś z kolejnych.