Podczas tworzenia aplikacji w JavaScript z pewnością natknąłeś się na funkcje, które działają inaczej niż tradycyjne. Mowa o funkcjach asynchronicznych, takich jak fetch
w przeglądarkach lub readFile
w Node.js.
Ich użycie w standardowy sposób może prowadzić do niespodziewanych rezultatów. Dlaczego? Ponieważ są to funkcje, które działają asynchronicznie. Ten artykuł przybliży Ci, co to oznacza i jak efektywnie korzystać z tych funkcji.
Funkcje Synchroniczne – Podstawy
JavaScript, działając w jednym wątku, wykonuje tylko jedno zadanie na raz. Jeżeli napotka funkcję, której wykonanie wymaga czasu, JavaScript wstrzymuje dalsze działanie do momentu jej zakończenia.
Większość funkcji jest przetwarzana w całości przez procesor. Oznacza to, że w trakcie ich działania procesor jest w pełni zaangażowany, niezależnie od czasu trwania. Takie funkcje nazywamy synchronicznymi. Spójrz na przykład poniżej:
function add(a, b) { for (let i = 0; i < 1000000; i ++) { // Nic nie robimy } return a + b; } // Wywołanie tej funkcji potrwa sum = add(10, 5); // Procesor nie przejdzie dalej console.log(sum);
Funkcja ta zawiera pętlę, której wykonanie zajmuje pewien czas, zanim zwróci sumę argumentów.
Po wywołaniu funkcji jej wynik zostaje przypisany do zmiennej sum
, a następnie wyświetlony w konsoli. Mimo, że obliczenie sumy zajmuje czas, procesor nie przejdzie do wyświetlenia jej, dopóki funkcja add
nie zakończy pracy.
Większość funkcji, z którymi się spotkasz, będzie działała właśnie w ten sposób. Jednak funkcje asynchroniczne zachowują się inaczej.
Funkcje Asynchroniczne – Wprowadzenie
Funkcje asynchroniczne wykonują większość pracy poza głównym procesorem. Dzięki temu, nawet jeśli proces trwa dłużej, procesor może wykonywać inne zadania.
Oto przykład:
fetch('https://jsonplaceholder.typicode.com/users/1');
Dla zwiększenia wydajności, JavaScript umożliwia procesorowi przejście do innych zadań, podczas gdy funkcja asynchroniczna oczekuje na zakończenie swojej pracy.
Ponieważ procesor przechodzi dalej, wynik funkcji asynchronicznej nie jest od razu dostępny, lecz jest w trakcie przetwarzania. Gdybyśmy próbowali użyć tego wyniku w dalszej części kodu, moglibyśmy natrafić na błędy.
Dlatego procesor powinien wykonywać jedynie te fragmenty kodu, które nie są zależne od wyniku asynchronicznego. Właśnie w tym celu współczesny JavaScript wykorzystuje obietnice.
Czym jest Obietnica w JavaScript?
Obietnica (ang. Promise) w JavaScript jest tymczasową wartością, zwracaną przez funkcję asynchroniczną. Stanowi fundament programowania asynchronicznego.
Gdy obietnica zostaje utworzona, może ona przyjąć jeden z dwóch stanów. Zostaje albo spełniona (ang. resolved), kiedy funkcja asynchroniczna zwróci poprawny wynik, albo odrzucona (ang. rejected) w przypadku wystąpienia błędu. Te dwa stany tworzą cykl życia obietnicy. Możemy dołączyć funkcje obsługi zdarzeń, które zostaną wykonane, kiedy obietnica zostanie spełniona lub odrzucona.
Kod, który potrzebuje wyniku funkcji asynchronicznej, umieszczamy w procedurze obsługi zdarzenia spełnionej obietnicy. Natomiast kod do obsługi błędów ląduje w procedurze obsługi zdarzenia odrzucenia.
Spójrz na przykład odczytu danych z pliku w Node.js:
const fs = require('fs/promises'); fileReadPromise = fs.readFile('./hello.txt', 'utf-8'); fileReadPromise.then((data) => console.log(data)); fileReadPromise.catch((error) => console.log(error));
W pierwszym wierszu importujemy moduł fs/promises
.
W drugim wierszu wywołujemy funkcję readFile
, przekazując nazwę pliku i kodowanie. Funkcja ta jest asynchroniczna, więc zwraca obietnicę, którą przypisujemy do zmiennej fileReadPromise
.
W trzecim wierszu dodajemy obsługę zdarzenia spełnienia obietnicy za pomocą metody then
. Przekazujemy funkcję, która ma zostać wykonana po pomyślnym odczycie danych.
W czwartym wierszu dodajemy obsługę zdarzenia odrzucenia obietnicy za pomocą metody catch
. Przekazujemy funkcję obsługi błędów.
Istnieje również alternatywne podejście z użyciem słów kluczowych async
i await
. Omówimy je w kolejnej sekcji.
async
i await
– Wyjaśnienie
Słowa kluczowe async
i await
pozwalają pisać kod asynchroniczny w bardziej czytelny sposób. Wyjaśnijmy, jak ich używać i jaki mają wpływ na kod.
Słowo await
wstrzymuje wykonanie funkcji, czekając na zakończenie funkcji asynchronicznej. Spójrz na przykład:
const fs = require('fs/promises'); function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // Ta linia zostanie wykonana dopiero po odczycie danych console.log(data); } readData()
Użyliśmy await
podczas wywoływania readFile
. Procesor poczeka, aż plik zostanie odczytany, zanim wykona kolejną linię (wyświetlenie w konsoli). Zapewnia to, że kod zależny od wyniku funkcji asynchronicznej nie zostanie wykonany, dopóki wynik nie będzie dostępny.
Jeśli spróbujesz uruchomić ten kod, zobaczysz błąd. Słowa await
można używać tylko wewnątrz funkcji asynchronicznej. Aby zadeklarować funkcję jako asynchroniczną, należy użyć słowa async
przed deklaracją funkcji:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // Ta linia zostanie wykonana dopiero po odczycie danych console.log(data); } // Wywołujemy funkcję, aby ją uruchomić readData() // Ten kod wykona się, podczas gdy funkcja readData będzie czekać console.log('Oczekiwanie na dane')
Po uruchomieniu tego kodu, zobaczysz, że JavaScript wykona zewnętrzny console.log
, czekając na dane z pliku. Gdy dane będą dostępne, wykona się console.log
wewnątrz readData
.
Obsługa błędów przy użyciu async
i await
odbywa się za pomocą bloków try...catch
. Warto również wiedzieć, jak używać async/await
w pętlach.
Słowa kluczowe async
i await
są dostępne w nowoczesnym JavaScript. Tradycyjnie kod asynchroniczny był pisany za pomocą tzw. callbacków.
Wprowadzenie do Callbacków
Callback (funkcja zwrotna) to funkcja, która jest wywoływana po otrzymaniu wyniku. Kod, który potrzebuje tego wyniku, umieszczamy wewnątrz tej funkcji. Reszta kodu, która nie zależy od wyniku, może być wykonana niezależnie.
Spójrzmy na przykład z odczytem pliku w Node.js:
const fs = require("fs"); fs.readFile("./hello.txt", "utf-8", (err, data) => { // Tutaj umieszczamy kod potrzebujący danych if (err) console.log(err); else console.log(data); }); // Ta część kodu wykona się bez czekania console.log("Witaj z programu")
W pierwszym wierszu importujemy moduł fs
. Następnie wywołujemy funkcję readFile
z modułu fs
. Funkcja ta odczyta tekst z pliku. Pierwszy argument to ścieżka do pliku, a drugi to format kodowania.
Funkcja readFile
odczytuje plik asynchronicznie. Przyjmuje funkcję jako trzeci argument. Ten argument to właśnie callback – funkcja, która zostanie wywołana po odczytaniu danych.
Pierwszym argumentem przekazanym do callbacku jest błąd, który wystąpi, jeśli coś pójdzie nie tak. Jeśli nie ma błędu, jego wartość będzie undefined
.
Drugim argumentem są dane odczytane z pliku. Kod wewnątrz callbacku będzie miał do nich dostęp. Kod poza callbackiem, nie zależy od danych z pliku, więc może być wykonany niezależnie.
Uruchomienie powyższego kodu da następujący wynik:
Kluczowe cechy JavaScript
Istnieje kilka kluczowych cech, które wpływają na działanie asynchronicznego JavaScript. Zostały one dobrze przedstawione w poniższym filmie:
Poniżej krótko omówię dwie ważne cechy.
#1. Jednowątkowość
W przeciwieństwie do innych języków, które pozwalają na używanie wielu wątków, JavaScript używa tylko jednego wątku. Wątek to sekwencja instrukcji, które zależą od siebie. Wiele wątków pozwala programowi wykonać inny wątek, jeśli napotka operację blokującą.
Wielowątkowość zwiększa jednak złożoność programu, co utrudnia zrozumienie kodu i zwiększa ryzyko błędów. Dlatego dla uproszczenia JavaScript jest jednowątkowy. Polega na obsłudze zdarzeń do efektywnej obsługi operacji blokujących.
#2. Sterowanie zdarzeniami
JavaScript jest również sterowany zdarzeniami. Niektóre zdarzenia mają miejsce podczas działania programu. Możesz dołączyć funkcje do tych zdarzeń i za każdym razem, gdy to zdarzenie wystąpi, dołączona funkcja zostanie wywołana.
Niektóre zdarzenia mogą wynikać z dostępności wyniku operacji blokującej. Wtedy powiązana funkcja jest wywoływana z tym wynikiem.
O czym pamiętać pisząc asynchroniczny JavaScript?
W tej ostatniej sekcji wspomnę o kilku rzeczach, które warto rozważyć podczas pracy z asynchronicznym JavaScriptem. Obejmują one obsługę przeglądarek, dobre praktyki i znaczenie tego podejścia.
Obsługa przeglądarek
Oto tabela przedstawiająca obsługę obietnic w różnych przeglądarkach:
Źródło: caniuse.com
Oto tabela z obsługą słów kluczowych async
w różnych przeglądarkach:
Źródło: caniuse.com
Dobre praktyki
- Zawsze wybieraj
async/await
, ponieważ pomaga to pisać bardziej przejrzysty i czytelny kod. - Obsługuj błędy za pomocą bloków
try/catch
. - Używaj słowa kluczowego
async
tylko wtedy, gdy musisz czekać na wynik funkcji.
Znaczenie kodu asynchronicznego
Kod asynchroniczny pozwala pisać bardziej efektywne programy, które działają w jednym wątku. Jest to kluczowe, ponieważ JavaScript jest używany do tworzenia stron internetowych, które wykonują wiele operacji asynchronicznych, takich jak żądania sieciowe, czy odczyt/zapis plików na dysku. Ta wydajność pozwoliła na wzrost popularności środowisk wykonawczych takich jak Node.js, jako preferowanego środowiska dla serwerów aplikacji.
Podsumowanie
To był długi artykuł, w którym udało nam się omówić różnicę między funkcjami synchronicznymi i asynchronicznymi. Przyjrzeliśmy się, jak pracować z kodem asynchronicznym za pomocą obietnic, async/await
i callbacków.
Omówiliśmy także kluczowe cechy JavaScript oraz wsparcie przeglądarek i dobre praktyki.
Na koniec, zachęcam do zapoznania się z najczęściej zadawanymi pytaniami na temat Node.js podczas rozmowy rekrutacyjnej.
newsblog.pl
Maciej – redaktor, pasjonat technologii i samozwańczy pogromca błędów w systemie Windows. Zna Linuxa lepiej niż własną lodówkę, a kawa to jego główne źródło zasilania. Pisze, testuje, naprawia – i czasem nawet wyłącza i włącza ponownie. W wolnych chwilach udaje, że odpoczywa, ale i tak kończy z laptopem na kolanach.