Co to jest i jak z niego korzystać

Photo of author

By maciekx

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