Witaj ponownie w Parku Jurajskim! Do tej pory skupialiśmy się na podstawach programowania obiektowego - konstrukcji klas, dziedziczeniu i kompozycji. Jednak w realnych aplikacjach, takich jak system zarządzania parkiem pełnym dinozaurów, często musimy radzić sobie z operacjami, które zajmują dużo czasu - od pobierania danych z czujników w wybiegach, komunikacji z systemami bezpieczeństwa, po obsługę interakcji z gośćmi parku.
W tym module przejdziemy do kluczowego aspektu JavaScript: jego asynchronicznej natury i modelu zdarzeń, na którym się opiera. Zrozumienie tego mechanizmu jest niezbędne dla budowania responsywnych i wydajnych aplikacji, które mogą obsługiwać wiele operacji jednocześnie - co jest kluczowe dla bezpieczeństwa parku pełnego drapieżników!
JavaScript jest językiem jednowątkowym (single-threaded). Oznacza to, że może wykonywać tylko jedną operację na raz. Jest to podobne do sytuacji, gdy masz tylko jednego pracownika do obsługi wszystkich zadań w parku.
Wyobraź sobie, że ten pracownik (wątek JavaScript) musi:
Gdyby wykonywał te zadania synchronicznie (jedno po drugim), musiałby całkowicie zakończyć jedno zadanie, zanim przejdzie do następnego. To byłoby niewydajne i potencjalnie niebezpieczne!
Na szczęście, JavaScript ma sprytny system zwany "event loop" (pętlą zdarzeń), który pozwala mu obsługiwać asynchroniczne operacje, mimo że jest jednowątkowy.
Aby zrozumieć event loop, wyobraźmy sobie centrum kontroli Parku Jurajskiego. W tym centrum mamy:
Zobaczmy, jak to działa na przykładzie zadań w Parku Jurajskim:
1console.log("1. Rozpoczynam poranny obchód parku.");
2
3setTimeout(() => {
4 console.log("4. Otrzymano dane z czujników wybiegów dinozaurów.");
5}, 2000);
6
7fetch('https://jurassic-park-api.com/security-status')
8 .then(response => response.json())
9 .then(data => {
10 console.log("5. Status systemów bezpieczeństwa:", data.status);
11 });
12
13console.log("2. Sprawdzam listę pracowników na dzisiaj.");
14console.log("3. Poranny obchód zakończony.");Jeśli uruchomimy ten kod, zobaczymy w konsoli:
11. Rozpoczynam poranny obchód parku.
22. Sprawdzam listę pracowników na dzisiaj.
33. Poranny obchód zakończony.
44. Otrzymano dane z czujników wybiegów dinozaurów.
55. Status systemów bezpieczeństwa: aktywnyCo się stało? Przeanalizujmy to krok po kroku:
console.log("1. Rozpoczynam poranny obchód parku.") jest dodawany do stosu wywołań, wykonywany i zdejmowany ze stosu.setTimeout(...) jest dodawany do stosu wywołań. JavaScript rozpoznaje, że to jest wywołanie asynchroniczne i przekazuje je do Web API, które startuje timer. setTimeout jest zdejmowany ze stosu.fetch(...) jest dodawany do stosu wywołań. JavaScript rozpoznaje, że to jest wywołanie asynchroniczne i przekazuje je do Web API, które wysyła żądanie HTTP. fetch jest zdejmowany ze stosu.console.log("2. Sprawdzam listę pracowników na dzisiaj.") jest dodawany do stosu, wykonywany i zdejmowany.console.log("3. Poranny obchód zakończony.") jest dodawany do stosu, wykonywany i zdejmowany.Stos wywołań jest teraz pusty, ale mamy dwie operacje asynchroniczne w toku:
setTimeoutfetchPo 2 sekundach timer kończy odliczanie i callback z
setTimeout jest dodawany do kolejki zadań (task queue).Event loop widzi, że stos wywołań jest pusty, więc przenosi callback z kolejki na stos.
console.log("4. Otrzymano dane z czujników wybiegów dinozaurów.") jest wykonywany i zdejmowany ze stosu.W międzyczasie żądanie HTTP się kończy, pobierane są dane i callback z
.then() jest dodawany do kolejki.Event loop znów przenosi callback z kolejki na stos.
Dane są przetwarzane i
console.log("5. Status systemów bezpieczeństwa:", data.status) jest wykonywany.Aby lepiej zrozumieć event loop, narysujmy jego główne komponenty:
1┌─────────────┐ ┌───────────┐ ┌───────────────────┐
2│ │ │ │ │ │
3│ Call Stack │ │ Web APIs │ │ Callback Queue │
4│ │ │ │ │ │
5└─────────────┘ └───────────┘ └───────────────────┘
6 ↑ │ │
7 │ ↓ │
8 │ ┌────────┐ │
9 └────────────│ Event │←─────────────┘
10 │ Loop │
11 └────────┘W graficznej reprezentacji:
Zadziwiające jest to, że cały ten proces działa na jednym wątku! JavaScript nie wykonuje tych zadań równolegle, ale przełącza się między nimi w taki sposób, że wydaje się, jakby wszystko działo się jednocześnie.
W nowszych implementacjach JavaScript (i specyfikacji) rozróżniamy dwa rodzaje kolejek:
Kiedy stos jest pusty, najpierw wykonywane są wszystkie zadania z kolejki mikrozadań, dopiero potem z kolejki makrozadań.
1console.log("1. Rozpoczynam poranne karmienie dinozaurów.");
2
3// Makrozadanie (setTimeout)
4setTimeout(() => {
5 console.log("4. Sprawdzam stan zdrowia dinozaurów.");
6}, 0);
7
8// Mikrozadanie (Promise)
9Promise.resolve().then(() => {
10 console.log("3. Odczyt danych z systemu bezpieczeństwa.");
11});
12
13console.log("2. Przygotowuję pokarm dla drapieżników.");Wynik:
11. Rozpoczynam poranne karmienie dinozaurów.
22. Przygotowuję pokarm dla drapieżników.
33. Odczyt danych z systemu bezpieczeństwa.
44. Sprawdzam stan zdrowia dinozaurów.Zauważ, że choć oba zadania asynchroniczne (
setTimeout z czasem 0ms i Promise.resolve()) są gotowe do wykonania niemal natychmiast, Promise (mikrozadanie) jest wykonywany przed setTimeout (makrozadaniem).Ponieważ JavaScript jest jednowątkowy, długotrwałe operacje mogą "zablokować" event loop, zatrzymując wykonywanie innych zadań. To jak sytuacja, gdy pracownik centrum kontroli utknął przy jednym monitorze i nie może sprawdzić pozostałych systemów.
1console.log("Uruchamiam symulację wzrostu dinozaurów...");
2
3// Funkcja blokująca wykonywanie przez 5 sekund
4function heavyComputation() {
5 const startTime = Date.now();
6 while (Date.now() - startTime < 5000) {
7 // Intensywne obliczenia...
8 }
9}
10
11heavyComputation();
12console.log("Symulacja zakończona!"); // Po 5 sekundach
13
14// Ten kod nie wykona się przez 5 sekund, nawet jeśli jest asynchroniczny!
15setTimeout(() => {
16 console.log("Sprawdzam systemy bezpieczeństwa...");
17}, 1000);W tym przykładzie funkcja
heavyComputation() blokuje wątek JavaScript na 5 sekund. W tym czasie żadne inne zadania nie mogą być wykonywane, nawet te asynchroniczne. To jak sytuacja, gdy pracownik parku utknął w jednym zadaniu i nie może reagować na alarmy czy inne ważne sygnały.W prawdziwym Parku Jurajskim takie blokowanie mogłoby być katastrofalne - raptor mógłby uciec, a system nie zarejestrowałby tego, bo jest zajęty czymś innym!
Dlatego w JavaScript staramy się:
Przeanalizujmy bardziej złożony przykład, który pokazuje kolejność wykonywania różnych typów zadań w Parku Jurajskim:
1// Symulacja systemu kontroli Parku Jurajskiego
2console.log("1. Uruchamianie systemu kontroli Parku Jurajskiego...");
3
4// Makrozadanie (setTimeout)
5setTimeout(() => {
6 console.log("6. Sprawdzanie ogrodzeń elektrycznych...");
7
8 // Zagnieżdżone mikrozadanie w makrozadaniu
9 Promise.resolve().then(() => {
10 console.log("7. Weryfikacja stanu drapieżników...");
11 });
12}, 0);
13
14// Mikrozadanie (Promise)
15Promise.resolve().then(() => {
16 console.log("3. Inicjalizacja kamer bezpieczeństwa...");
17
18 // Zagnieżdżone mikrozadanie
19 Promise.resolve().then(() => {
20 console.log("4. Kalibracja czujników ruchu...");
21 });
22});
23
24// Kolejne mikrozadanie
25Promise.resolve().then(() => {
26 console.log("5. Uruchamianie protokołów awaryjnych...");
27});
28
29// Synchroniczny kod
30console.log("2. Sprawdzanie statusu systemów...");Wynik:
11. Uruchamianie systemu kontroli Parku Jurajskiego...
22. Sprawdzanie statusu systemów...
33. Inicjalizacja kamer bezpieczeństwa...
44. Kalibracja czujników ruchu...
55. Uruchamianie protokołów awaryjnych...
66. Sprawdzanie ogrodzeń elektrycznych...
77. Weryfikacja stanu drapieżników...Ten przykład pokazuje, że:
Przy pracy z kodem asynchronicznym ważne jest, aby poprawnie deklarować zmienne. Sposób deklaracji wpływa na ich zachowanie w blokach i funkcjach, co ma bezpośredni wpływ na kod asynchroniczny (np. zmienne w pętlach z setTimeout). Podobnie jak w parku mamy różne rodzaje ogrodzeń dla różnych typów dinozaurów (elektryczne dla T-Rexa, standardowe dla Triceratopsa), w JavaScript mamy różne sposoby deklarowania zmiennych:
var, let i const.const oznacza stałą - wartość, która nie może być ponownie przypisana. W Parku Jurajskim używamy const dla rzeczy, które nie powinny się zmieniać, jak parametry bezpieczeństwa czy konfiguracja systemów:1// Konfiguracja systemu - nie powinna się zmieniać
2const MAX_DINOSAURS_PER_ENCLOSURE = 5;
3const FENCE_MINIMUM_VOLTAGE = 10000; // wolty
4const EMERGENCY_CONTACT = "+1-555-JURASSIC";
5
6// Próba zmiany const spowoduje błąd
7MAX_DINOSAURS_PER_ENCLOSURE = 10; // TypeError: Assignment to constant variableKiedy używać const:
constWażne:
const nie oznacza niezmienności zawartości obiektu lub tablicy!1// To jest poprawne - modyfikujemy zawartość, nie przypisanie
2const dinosaurs = [];
3dinosaurs.push({ name: "Rexy", species: "T-Rex" }); // OK!
4dinosaurs.push({ name: "Blue", species: "Velociraptor" }); // OK!
5
6const parkConfig = {
7 openTime: "09:00",
8 closeTime: "18:00"
9};
10parkConfig.closeTime = "20:00"; // OK - modyfikujemy właściwość
11
12// To spowoduje błąd - próbujemy ponownie przypisać
13dinosaurs = []; // TypeError!
14parkConfig = {}; // TypeError!let używamy, gdy wartość zmiennej będzie się zmieniać w trakcie wykonania programu. W Parku Jurajskim używamy let dla rzeczy dynamicznych, jak liczniki, akumulatory czy zmienne pętli:1// Licznik odwiedzających - zmienia się w ciągu dnia
2let visitorCount = 0;
3
4function addVisitors(count) {
5 visitorCount += count;
6 console.log(`Aktualna liczba odwiedzających: ${visitorCount}`);
7}
8
9addVisitors(50); // 50
10addVisitors(30); // 80
11addVisitors(20); // 100Kiedy używać let:
1// Pętle - klasyczny przypadek użycia let
2for (let i = 0; i < 5; i++) {
3 console.log(`Sprawdzam dinozaura nr ${i}`);
4}
5
6// Akumulatory
7let totalFeedingCost = 0;
8
9const dinosaurs = [
10 { name: "Rexy", dailyFoodCost: 500 },
11 { name: "Blue", dailyFoodCost: 150 },
12 { name: "Trixie", dailyFoodCost: 200 }
13];
14
15for (const dino of dinosaurs) {
16 totalFeedingCost += dino.dailyFoodCost;
17}
18
19console.log(`Całkowity koszt karmienia: $${totalFeedingCost}`);var to stary sposób deklarowania zmiennych w JavaScript. Ma problematyczne zachowanie i nie powinien być używany w nowoczesnym kodzie. Jedyny czas, gdy zobaczysz var, to w starym kodzie (legacy), który został napisany przed 2015 rokiem (przed ES6).Problemy z var:
var "wydostaje się" z bloków1// Problem: var ignoruje bloki
2function checkDinosaur(isDangerous) {
3 if (isDangerous) {
4 var alert = "NIEBEZPIECZEŃSTWO!"; // var jest dostępny poza if!
5 }
6
7 console.log(alert); // "NIEBEZPIECZEŃSTWO!" - przecieka poza if!
8 // Z let/const dostalibyśmy ReferenceError
9}
10
11checkDinosaur(true);
12
13// Porównaj z let:
14function checkDinosaurProper(isDangerous) {
15 if (isDangerous) {
16 let alert = "NIEBEZPIECZEŃSTWO!";
17 }
18
19 console.log(alert); // ReferenceError: alert is not defined
20}var jest "podnoszony" na górę funkcji1// Dziwne zachowanie var
2function feedDinosaurs() {
3 console.log(food); // undefined (nie ReferenceError!)
4 var food = "meat";
5 console.log(food); // "meat"
6}
7
8// To jest interpretowane jako:
9function feedDinosaurs() {
10 var food; // hoisting - deklaracja na górze
11 console.log(food); // undefined
12 food = "meat"; // przypisanie
13 console.log(food); // "meat"
14}1var dinosaurName = "Rexy";
2// Przypadkowo używamy tej samej nazwy
3var dinosaurName = "Blue"; // Brak błędu! Nadpisało poprzednią wartość
4
5console.log(dinosaurName); // "Blue" - Rexy zniknął!
6
7// Z let/const:
8let dinoName = "Rexy";
9let dinoName = "Blue"; // SyntaxError: Identifier 'dinoName' has already been declaredKiedy używać var:
Wyobraź sobie, że dostajesz kod od kolegi do przejrzenia. Oto jak poprawić użycie zmiennych:
Zły kod (przed):
1// Źle - wszystko na var
2var parkName = "Jurassic Park"; // Nie zmienia się - powinno być const
3var isOpen = true; // Zmienia się - powinno być let
4var maxCapacity = 10000; // Nie zmienia się - powinno być const
5
6function processVisitors() {
7 var totalVisitors = 0; // Akumulator - powinno być let
8
9 for (var i = 0; i < 100; i++) { // Iterator - powinno być let
10 totalVisitors++;
11 }
12
13 console.log(i); // 100 - var przecieka! Problem!
14 return totalVisitors;
15}Dobry kod (po):
1// Dobrze - używamy const jako domyślnego
2const parkName = "Jurassic Park";
3const maxCapacity = 10000;
4
5// let tylko gdy wartość się zmienia
6let isOpen = true;
7
8function processVisitors() {
9 // let dla akumulatora
10 let totalVisitors = 0;
11
12 // let w pętli - block scoped
13 for (let i = 0; i < 100; i++) {
14 totalVisitors++;
15 }
16
17 // console.log(i); // ReferenceError - i jest w scope pętli
18 return totalVisitors;
19}
20
21// Możemy zmieniać isOpen
22isOpen = false;
23isOpen = true;
24
25// Ale nie możemy zmienić const
26// parkName = "Dino World"; // TypeError!1class DinosaurManagementSystem {
2 constructor() {
3 // const dla właściwości, które nie zmieniają referencji
4 this.dinosaurs = []; // tablica może się zmieniać, ale referencja nie
5 this.config = { // obiekt może się zmieniać, ale referencja nie
6 maxDinosaurs: 50,
7 securityLevel: "high"
8 };
9 }
10
11 addDinosaur(name, species, dangerLevel) {
12 // const dla parametrów funkcji i lokalnych wartości, które się nie zmieniają
13 const newDinosaur = {
14 id: Date.now(),
15 name,
16 species,
17 dangerLevel,
18 status: "active"
19 };
20
21 this.dinosaurs.push(newDinosaur);
22 return newDinosaur;
23 }
24
25 getDangerousCount() {
26 // let dla akumulatora
27 let count = 0;
28
29 // const w for...of gdy nie modyfikujemy zmiennej pętli
30 for (const dino of this.dinosaurs) {
31 if (dino.dangerLevel > 7) {
32 count++; // modyfikujemy count, dlatego let
33 }
34 }
35
36 return count;
37 }
38
39 calculateDailyFoodCost() {
40 // let dla wartości, które się zmieniają
41 let totalCost = 0;
42
43 // let w klasycznej pętli for
44 for (let i = 0; i < this.dinosaurs.length; i++) {
45 const dino = this.dinosaurs[i]; // const - nie zmieniamy referencji
46
47 // const dla wartości obliczonych
48 const dailyCost = this.calculateDinosaurFoodCost(dino);
49 totalCost += dailyCost;
50 }
51
52 return totalCost;
53 }
54
55 calculateDinosaurFoodCost(dinosaur) {
56 // const dla wszystkich wartości, które się nie zmieniają
57 const baseCost = 50;
58 const costMultiplier = dinosaur.dangerLevel > 5 ? 2 : 1;
59 const dailyCost = baseCost * costMultiplier;
60
61 return dailyCost;
62 }
63}
64
65// Przykład użycia
66const parkSystem = new DinosaurManagementSystem(); // const - referencja się nie zmienia
67
68parkSystem.addDinosaur("Rexy", "T-Rex", 9);
69parkSystem.addDinosaur("Blue", "Velociraptor", 8);
70parkSystem.addDinosaur("Trixie", "Triceratops", 3);
71
72// let dla wartości, które będą się zmieniać
73let dangerousCount = parkSystem.getDangerousCount();
74console.log(`Niebezpieczne dinozaury: ${dangerousCount}`);
75
76// const dla wartości jednorazowego użycia
77const foodCost = parkSystem.calculateDailyFoodCost();
78console.log(`Dzienny koszt jedzenia: $${foodCost}`);
79
80// Możemy dodać więcej dinozaurów
81parkSystem.addDinosaur("Delta", "Velociraptor", 8);
82
83// I ponownie obliczyć
84dangerousCount = parkSystem.getDangerousCount(); // let pozwala na zmianę
85console.log(`Niebezpieczne dinozaury: ${dangerousCount}`);const - jeśli kompilator/linter narzeka, że próbujesz zmienić wartość, zmień na letlet tylko gdy musisz - pętle, liczniki, akumulatoryvar - chyba że edytujesz stary kod1// ✅ Dobra praktyka
2const maxDinosaurs = 50;
3let currentDinosaurs = 0;
4
5for (let i = 0; i < maxDinosaurs; i++) {
6 const dinosaur = createDinosaur();
7 currentDinosaurs++;
8}
9
10// ❌ Zła praktyka
11var maxDinosaurs = 50;
12var currentDinosaurs = 0;
13
14for (var i = 0; i < maxDinosaurs; i++) {
15 var dinosaur = createDinosaur();
16 currentDinosaurs++;
17}Podobnie jak prawidłowe ogrodzenia w Parku Jurajskim zapobiegają ucieczkom dinozaurów, prawidłowe używanie
const i let zapobiega błędom w kodzie:let) a które nie (const)const chroni przed przypadkową modyfikacjąZapamiętaj: w nowoczesnym JavaScript,
var to jak dinozaury - interesujący relikt przeszłości, ale nie chcesz go w swoim produkcyjnym kodzie!Model zdarzeń JavaScript (event loop) jest kluczem do zrozumienia, jak ten jednowątkowy język może obsługiwać wiele operacji asynchronicznych, co jest niezbędne w aplikacjach takich jak system zarządzania Parkiem Jurajskim.
Pamiętaj kluczowe zasady:
W kolejnych lekcjach zobaczymy, jak wykorzystać tę wiedzę do pisania wydajnego kodu asynchronicznego za pomocą callbacków, Promises i async/await.