Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Model zdarzeń (event loop)

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 - język jednowątkowy z asynchronicznymi możliwościami

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:

  • Sprawdzać stan ogrodzeń
  • Monitorować znaki życiowe dinozaurów
  • Odpowiadać na komunikaty od gości
  • Kontrolować systemy awaryjne

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.

Event Loop - serce asynchronicznego JavaScript

Aby zrozumieć event loop, wyobraźmy sobie centrum kontroli Parku Jurajskiego. W tym centrum mamy:

  1. Stos wywołań (Call Stack) - lista aktualnie wykonywanych zadań (główne zadania naszego pracownika)
  2. Kolejka wywołań zwrotnych (Callback Queue) - lista zadań czekających na wykonanie po zakończeniu operacji asynchronicznych
  3. Web APIs (w przeglądarkach) lub C++ APIs (w Node.js) - zewnętrzne systemy, które mogą wykonywać zadania "w tle"
  4. Event Loop - mechanizm, który ciągle sprawdza, czy stos wywołań jest pusty, i jeśli tak, przenosi zadania z kolejki na stos

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

Co się stało? Przeanalizujmy to krok po kroku:

  1. console.log("1. Rozpoczynam poranny obchód parku.")
    jest dodawany do stosu wywołań, wykonywany i zdejmowany ze stosu.

  2. 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.

  3. 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.

  4. console.log("2. Sprawdzam listę pracowników na dzisiaj.")
    jest dodawany do stosu, wykonywany i zdejmowany.

  5. console.log("3. Poranny obchód zakończony.")
    jest dodawany do stosu, wykonywany i zdejmowany.

  6. Stos wywołań jest teraz pusty, ale mamy dwie operacje asynchroniczne w toku:

    • Timer od
      setTimeout
    • Żądanie HTTP od
      fetch
  7. Po 2 sekundach timer kończy odliczanie i callback z

    setTimeout
    jest dodawany do kolejki zadań (task queue).

  8. Event loop widzi, że stos wywołań jest pusty, więc przenosi callback z kolejki na stos.

  9. console.log("4. Otrzymano dane z czujników wybiegów dinozaurów.")
    jest wykonywany i zdejmowany ze stosu.

  10. W międzyczasie żądanie HTTP się kończy, pobierane są dane i callback z

    .then()
    jest dodawany do kolejki.

  11. Event loop znów przenosi callback z kolejki na stos.

  12. Dane są przetwarzane i

    console.log("5. Status systemów bezpieczeństwa:", data.status)
    jest wykonywany.

Event Loop - szczegółowa analiza

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:

  1. Call Stack - Stos LIFO (Last In, First Out) przechowujący wywołania funkcji, które są obecnie wykonywane
  2. Web APIs - Interfejsy przeglądarki/środowiska dla operacji asynchronicznych (setTimeout, fetch, Event Listeners)
  3. Callback Queue - Kolejka FIFO (First In, First Out) przechowująca callbacki, które są gotowe do wykonania
  4. Event Loop - Ciągłe sprawdzanie, czy stos wywołań jest pusty, i jeśli tak, przenoszenie funkcji z kolejki na stos

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.

Mikrozadania i makrozadania

W nowszych implementacjach JavaScript (i specyfikacji) rozróżniamy dwa rodzaje kolejek:

  1. Kolejka mikrozadań (Microtask Queue) - dla zadań o wyższym priorytecie, jak Promise callbacks
  2. Kolejka makrozadań (Macrotask Queue/Task Queue) - dla zadań o niższym priorytecie, jak setTimeout, setInterval, I/O

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).

Blokowanie Event Loop

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

  1. Unikać długich operacji synchronicznych
  2. Dzielić złożone obliczenia na mniejsze części
  3. Wykorzystywać Web Workers do operacji intensywnie wykorzystujących CPU

Kolejność wykonywania zadań - praktyczny przykład

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:

  1. Najpierw wykonywany jest kod synchroniczny (1, 2)
  2. Następnie wykonywane są wszystkie zadania z kolejki mikrozadań (3, 4, 5)
  3. Dopiero potem wykonywane są zadania z kolejki makrozadań (6)
  4. Mikrozadania stworzone podczas wykonywania makrozadania (7) są wykonywane natychmiast po zakończeniu bieżącego makrozadania

Bonus: Var, Let, Const - kiedy używać którego?

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 - zawsze domyślny wybór

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 variable

Kiedy używać const:

  • Dla wartości, które nie będą zmieniane
  • Dla obiektów i tablic (nawet jeśli ich zawartość się zmienia)
  • Jako domyślny wybór - jeśli nie jesteś pewien, użyj
    const

Waż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 - gdy wartość się zmienia

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); // 100

Kiedy używać let:

  • W pętlach (for, while)
  • Dla liczników i akumulatorów
  • Gdy wartość będzie ponownie przypisywana
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 - prawie nigdy (legacy code)

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:

  1. Function scope zamiast block scope -
    var
    "wydostaje się" z bloków
1// 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}
  1. Hoisting z undefined -
    var
    jest "podnoszony" na górę funkcji
1// 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}
  1. Można redekla rować - można przypadkowo nadpisać zmienne
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 declared

Kiedy używać var:

  • Nigdy w nowym kodzie
  • Tylko gdy edytujesz bardzo stary kod i musisz zachować kompatybilność

Praktyczny przykład - Code Review

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!

Kolejny przykład - System zarządzania dinozaurami

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}`);

Zasada kciuka (Rule of Thumb)

  1. Zawsze zaczyń od
    const
    - jeśli kompilator/linter narzeka, że próbujesz zmienić wartość, zmień na
    let
  2. Użyj
    let
    tylko gdy musisz
    - pętle, liczniki, akumulatory
  3. Nigdy nie używaj
    var
    - chyba że edytujesz stary kod
1// ✅ 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}

Dlaczego to ważne?

Podobnie jak prawidłowe ogrodzenia w Parku Jurajskim zapobiegają ucieczkom dinozaurów, prawidłowe używanie

const
i
let
zapobiega błędom w kodzie:

  1. Czytelność - widzisz natychmiast, które wartości się zmieniają (
    let
    ) a które nie (
    const
    )
  2. Bezpieczeństwo -
    const
    chroni przed przypadkową modyfikacją
  3. Block scope - zmienne nie "przeciekają" z bloków
  4. Łatwiejsze debugowanie - mniej niespodzianek z hoistingiem

Zapamiętaj: w nowoczesnym JavaScript,

var
to jak dinozaury - interesujący relikt przeszłości, ale nie chcesz go w swoim produkcyjnym kodzie!

Podsumowanie

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:

  1. JavaScript jest jednowątkowy, ale asynchroniczny
  2. Event loop pozwala na obsługę wielu zadań bez blokowania interfejsu użytkownika
  3. Zadania są wykonywane w określonej kolejności: synchroniczne → mikrozadania → makrozadania
  4. Unikaj blokowania event loop długimi operacjami synchronicznymi

W kolejnych lekcjach zobaczymy, jak wykorzystać tę wiedzę do pisania wydajnego kodu asynchronicznego za pomocą callbacków, Promises i async/await.

Przejdź do CodeWorlds