Temat 29 · 2 × 45 min
Event loop, callbacki i Promise
JavaScript w przeglądarce ma jeden wątek: w danej chwili wykonuje się dokładnie jedna funkcja. A jednak strona nie zamiera, gdy czeka na odpowiedź serwera, odlicza timer albo animuje slider. Jak to możliwe? Odpowiedzią jest pętla zdarzeń (event loop) – mechanizm, który decyduje, co i kiedy się wykona. Zrozumiesz go raz i przestaniesz zgadywać, dlaczego console.log wypisały się „w złej kolejności”. Potem poznasz Promise – obiekt, który porządkuje kod asynchroniczny i jest fundamentem fetch oraz async/await z lekcji 30.
Jeden wątek, zero czekania
Spróbuj zablokować wątek – zobaczysz, co to znaczy „synchronicznie”:
console.log('start');
const start = Date.now();
while (Date.now() - start < 300) {
// pusta pętla przez 300 ms – NIC innego nie może się wykonać: ani kliknięcie, ani odświeżenie ekranu
}
console.log('po 300 ms blokady');Gdyby pobieranie danych z serwera działało tak samo (czekaj w pętli, aż przyjdą), strona zamarzałaby na każde żądanie. Dlatego operacje, które trwają – timery, sieć, odczyt pliku, oczekiwanie na kliknięcie – są asynchroniczne: zlecasz je przeglądarce, podajesz funkcję do wywołania „gdy skończysz” i idziesz dalej. Przeglądarka robi robotę obok (w swoich wątkach), a wynik oddaje twojemu jedynemu wątkowi JS przez kolejkę.
Model: stos, kolejki i pętla zdarzeń
Cztery elementy, które trzeba znać:
| Element | Co to jest | Co tam trafia |
|---|---|---|
| Stos wywołań (call stack) | lista funkcji aktualnie wykonywanych; ostatnio wywołana na górze | cały twój synchroniczny kod |
| Web API | części przeglądarki pracujące równolegle z JS | timery, fetch, oczekiwanie na zdarzenia DOM |
| Kolejka zadań (task queue, makrozadania) | kolejka callbacków czekających na pusty stos | setTimeout/setInterval, zdarzenia (click), odpowiedź sieci |
| Kolejka mikrozadań (microtask queue) | druga kolejka – z pierwszeństwem | callbacki then/catch/finally, await, queueMicrotask |
Pętla zdarzeń powtarza w nieskończoność:
- Wykonaj to, co na stosie, do końca (nic nie może przerwać działającej funkcji).
- Opróżnij całą kolejkę mikrozadań – także mikrozadania dodane w trakcie opróżniania.
- Jeśli pora – odmaluj stronę (render).
- Weź jedno zadanie z kolejki zadań, wrzuć na stos. Wróć do 1.
Z tych czterech zdań wynikają wszystkie „zagadki” o kolejności. Przetestuj się:
Co wypisze ten kod?
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);console.log('1 – synchronicznie');
setTimeout(() => console.log('5 – zadanie (setTimeout 0 ms)'), 0);
Promise.resolve().then(() => console.log('3 – mikrozadanie (then)'));
queueMicrotask(() => console.log('4 – mikrozadanie (queueMicrotask)'));
console.log('2 – synchronicznie, koniec skryptu');Mikrozadania wykonują się w kolejności dodania (3 przed 4), ale wszystkie przed pierwszym zadaniem z timera. Praktyczny wniosek: setTimeout(f, 0) znaczy „po wszystkim, co teraz czeka – w osobnym zadaniu, przed którym przeglądarka może (ale nie musi) odmalować ekran”, a Promise.resolve().then(f) – „natychmiast po bieżącym kodzie, na pewno przed odmalowaniem”.
Co wypisze ten kod?
setTimeout(() => console.log('A'), 0);
Promise.resolve()
.then(() => console.log('B'))
.then(() => console.log('C'));
console.log('D');Callbacki i piramida zagłady
Callback (funkcja zwrotna) to funkcja przekazana innej funkcji, by ta wywołała ją później. Używasz ich od lekcji 12: addEventListener('click', f), setTimeout(f, 1000), tablica.forEach(f). Problem zaczyna się, gdy operacje muszą iść po kolei, a każda zależy od poprzedniej. Symulujmy serwer, który odpowiada po chwili:
// „serwer”: po 200 ms oddaje dane przez callback (konwencja: pierwszy argument to błąd lub null)
function pobierzUzytkownika(id, callback) {
setTimeout(() => callback(null, { id, imie: 'Ola' }), 200);
}
function pobierzZamowienia(uzytkownikId, callback) {
setTimeout(() => callback(null, [{ nr: 1, kwota: 120 }, { nr: 2, kwota: 80 }]), 200);
}
function pobierzFakture(nrZamowienia, callback) {
setTimeout(() => callback(null, `FV/2026/${nrZamowienia}`), 200);
}
// trzy kroki w zależności: użytkownik → jego zamówienia → faktura do pierwszego
pobierzUzytkownika(7, (blad, uzytkownik) => {
if (blad) return console.error(blad);
pobierzZamowienia(uzytkownik.id, (blad, zamowienia) => {
if (blad) return console.error(blad);
pobierzFakture(zamowienia[0].nr, (blad, faktura) => {
if (blad) return console.error(blad);
console.log(`${uzytkownik.imie} – ${zamowienia.length} zamówienia – faktura ${faktura}`);
// a gdyby był czwarty krok? piąty? kod ucieka w prawo…
});
});
});
console.log('To wypisze się PIERWSZE – callbacki czekają na timery');Działa, ale: każdy krok to kolejny poziom wcięcia, obsługa błędu powtarza się trzykrotnie, nie da się łatwo „poczekać na dwa równolegle”, a gdy pobierzFakture wywoła callback dwa razy (błąd w bibliotece) – kod też wykona się dwa razy. Ten kształt ma swoją nazwę: callback hell albo piramida zagłady. Lekarstwem jest Promise.
Promise – obietnica wyniku
Promise (obietnica) to obiekt reprezentujący wynik, który pojawi się później. Zamiast podawać callback do funkcji, dostajesz od niej obiekt, do którego sam podpinasz, co ma się stać po sukcesie i po błędzie. Obietnica ma trzy stany:
| Stan | Znaczenie | Jak się tam dostać |
|---|---|---|
pending (oczekująca) | operacja trwa | stan początkowy |
fulfilled (spełniona) | sukces, jest wartość | wywołanie resolve(wartosc) |
rejected (odrzucona) | błąd, jest powód | wywołanie reject(blad) lub rzucenie wyjątku |
Stan zmienia się tylko raz – spełnionej obietnicy nie da się odrzucić ani zmienić jej wartości. Spełniona lub odrzucona obietnica jest „rozstrzygnięta” (settled).
// Funkcja, która ZWRACA obietnicę zamiast przyjmować callback.
// Konstruktor dostaje executor – funkcję z dwoma „przełącznikami”: resolve i reject.
function czekaj(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(`minęło ${ms} ms`), ms);
});
}
function rzutKostka() {
return new Promise((resolve, reject) => {
const oczko = Math.floor(Math.random() * 6) + 1;
setTimeout(() => {
if (oczko === 6) reject(new Error('Szóstka – gracz traci kolejkę'));
else resolve(oczko);
}, 100);
});
}
const obietnica = czekaj(300);
console.log(obietnica instanceof Promise); // true
console.log(String(obietnica)); // [object Promise] – stanu nie odczytasz wprost, tylko przez then
obietnica.then((wynik) => console.log('then:', wynik)); // then: minęło 300 ms
rzutKostka()
.then((oczko) => console.log('wyrzucono', oczko))
.catch((blad) => console.log('catch:', blad.message))
.finally(() => console.log('finally: tura zakończona (zawsze)'));Trzy metody konsumenta:
then(naSukces, naBlad)– najczęściej z jednym argumentem; zwraca nową obietnicę (klucz do łańcuchowania).catch(naBlad)– skrót nathen(undefined, naBlad); łapie odrzucenie i wyjątki rzucone w poprzednichthen.finally(f)– wykonuje się zawsze, nie dostaje wartości i przepuszcza ją dalej; miejsce na „ukryj spinner”.
Co wypisze ten kod i w jakiej kolejności?
const p = new Promise((resolve) => {
console.log('A');
resolve('B');
});
p.then((w) => console.log(w));
console.log('C');Łańcuchowanie: then zwraca obietnicę
To tutaj Promise wygrywa z callbackami. then zwraca nową obietnicę, której wartość zależy od tego, co zwrócisz z callbacka:
- zwrócisz zwykłą wartość → następny
thendostanie tę wartość, - zwrócisz obietnicę → następny
thenpoczeka na nią i dostanie jej wynik (obietnica „się rozpakowuje”), - rzucisz wyjątek → łańcuch skacze do najbliższego
catch, - nie zwrócisz nic → następny
thendostanieundefined.
function pobierzUzytkownika(id) {
return new Promise((resolve) => setTimeout(() => resolve({ id, imie: 'Ola' }), 200));
}
function pobierzZamowienia(uzytkownikId) {
return new Promise((resolve) => setTimeout(() => resolve([{ nr: 1, kwota: 120 }, { nr: 2, kwota: 80 }]), 200));
}
function pobierzFakture(nr) {
return new Promise((resolve) => setTimeout(() => resolve(`FV/2026/${nr}`), 200));
}
// Ta sama logika co w piramidzie – ale płasko, z jedną obsługą błędów
pobierzUzytkownika(7)
.then((uzytkownik) => {
console.log('mam użytkownika', uzytkownik.imie);
return pobierzZamowienia(uzytkownik.id); // zwracamy OBIETNICĘ – następny then na nią poczeka
})
.then((zamowienia) => {
const suma = zamowienia.reduce((acc, z) => acc + z.kwota, 0);
console.log('suma zamówień:', suma);
return pobierzFakture(zamowienia[0].nr);
})
.then((faktura) => {
console.log('faktura:', faktura);
return faktura.length; // zwykła wartość
})
.then((dlugosc) => console.log('długość numeru:', dlugosc)) // 9
.catch((blad) => console.error('Coś poszło nie tak:', blad.message))
.finally(() => console.log('koniec łańcucha'));Co wypisze ostatni then?
Promise.resolve(2)
.then((x) => x * 3)
.then((x) => { x + 10; })
.then((x) => console.log(x));Błędy w łańcuchu
Wyjątek rzucony w którymkolwiek then (albo odrzucona obietnica zwrócona z then) przeskakuje wszystkie kolejne then i ląduje w najbliższym catch. Po catch łańcuch żyje dalej – catch też zwraca obietnicę, więc możesz podać wartość zastępczą:
function parsujKwote(tekst) {
return new Promise((resolve, reject) => {
const n = Number(tekst);
Number.isNaN(n) ? reject(new TypeError(`„${tekst}” to nie liczba`)) : resolve(n);
});
}
parsujKwote('abc')
.then((n) => {
console.log('to się NIE wykona', n);
return n * 2;
})
.catch((blad) => {
console.log('złapano:', blad.message); // złapano: „abc” to nie liczba
return 0; // wartość zastępcza – łańcuch wraca na tory
})
.then((n) => console.log('dalej z wartością', n)); // dalej z wartością 0
// wyjątek rzucony w then = odrzucenie
Promise.resolve('{ zly json')
.then((tekst) => JSON.parse(tekst)) // SyntaxError
.then(() => console.log('pominięte'))
.catch((e) => console.log('catch dostał:', e.name)); // catch dostał: SyntaxError
// kolejność: catch dla pierwszego błędu, a potem łańcuch idzie dalej
Promise.reject(new Error('pierwszy'))
.catch((e) => console.log('1.', e.message))
.then(() => { throw new Error('drugi'); })
.catch((e) => console.log('2.', e.message))
.finally(() => console.log('finally bez argumentu'));Obietnica odrzucona bez żadnego catch to w konsoli przeglądarki Uncaught (in promise) Error: …. Zasada: każdy łańcuch kończy się catch (lub try…catch wokół await w lekcji 30). Nie łap błędów „po cichu” pustym catch(() => {}) – choćby console.error musi zostać.
Promisyfikacja: z callbacków na obietnice
Stare API (callbacki) opakowujesz w funkcję zwracającą Promise. Najczęstszy przykład to czekaj(ms) – „obietnicowy setTimeout”, którego będziesz używać do testów, animacji i opóźnień:
// 1. setTimeout → obietnica
const czekaj = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
czekaj(200).then(() => console.log('po 200 ms'));
// 2. funkcja z callbackiem (blad, wynik) → obietnica
function pobierzStary(id, callback) {
setTimeout(() => (id > 0 ? callback(null, { id }) : callback(new Error('zły id'))), 100);
}
function pobierz(id) {
return new Promise((resolve, reject) => {
pobierzStary(id, (blad, wynik) => (blad ? reject(blad) : resolve(wynik)));
});
}
pobierz(5).then((w) => console.log('pobrano', w));
pobierz(-1).catch((e) => console.log('błąd:', e.message));
// 3. Promise.withResolvers() – gdy resolve/reject potrzebujesz POZA executorem (np. „czekaj na kliknięcie”)
const { promise, resolve } = Promise.withResolvers();
promise.then((v) => console.log('rozstrzygnięto z zewnątrz:', v));
setTimeout(() => resolve('klik!'), 300);Promise.withResolvers() jest dostępne we wszystkich aktualnych przeglądarkach od marca 2024. W starszym kodzie zobaczysz to samo zrobione ręcznie: let resolve; const p = new Promise((r) => (resolve = r));.
Wiele obietnic naraz: Promise.all i spółka
Łańcuch jest sekwencyjny: krok po kroku. Gdy operacje są niezależne (trzy różne zapytania do API), uruchom je równolegle i poczekaj na wszystkie:
| Metoda | Spełnia się, gdy… | Odrzuca, gdy… | Wynik | Użyj do |
|---|---|---|---|---|
Promise.all([…]) | wszystkie spełnione | którakolwiek odrzucona (natychmiast) | tablica wyników w kolejności wejścia | kilka niezbędnych zapytań |
Promise.allSettled([…]) | wszystkie rozstrzygnięte (sukces lub błąd) | nigdy | tablica { status, value } / { status, reason } | raport: co się udało, co nie |
Promise.race([…]) | pierwsza rozstrzygnięta | …jeśli ta pierwsza była odrzucona | wynik pierwszej | timeout: race([zapytanie, czekaj(5000)]) |
Promise.any([…]) | pierwsza spełniona | wszystkie odrzucone (AggregateError) | wynik pierwszej udanej | kilka serwerów lustrzanych |
const czekaj = (ms, wartosc) => new Promise((resolve) => setTimeout(() => resolve(wartosc), ms));
const odrzuc = (ms, powod) => new Promise((_, reject) => setTimeout(() => reject(new Error(powod)), ms));
const start = Date.now();
// all: trzy „zapytania” równolegle – całość trwa tyle, co najdłuższe (300 ms), nie 600
Promise.all([czekaj(300, 'produkty'), czekaj(100, 'kategorie'), czekaj(200, 'koszyk')]).then((wyniki) => {
console.log('all:', wyniki, `po ~${Math.round((Date.now() - start) / 100) * 100} ms`);
// all: ["produkty","kategorie","koszyk"] po ~300 ms – kolejność jak na wejściu, nie wg czasu
});
// all z błędem: odrzuca od razu, wyniki pozostałych przepadają
Promise.all([czekaj(400, 'a'), odrzuc(100, 'serwer padł')]).catch((e) => console.log('all catch:', e.message));
// allSettled: raport z każdej
Promise.allSettled([czekaj(100, 'ok'), odrzuc(100, 'nie ok')]).then((r) =>
console.log('allSettled:', r.map((x) => x.status)) // ["fulfilled","rejected"]
);
// race: kto pierwszy – klasyczny timeout
Promise.race([czekaj(500, 'dane'), odrzuc(200, 'przekroczono czas')]).catch((e) => console.log('race:', e.message));
// any: pierwsza UDANA, błędy po drodze nie przeszkadzają
Promise.any([odrzuc(50, 'serwer 1'), czekaj(150, 'serwer 2 odpowiedział')]).then((w) => console.log('any:', w));Trzy zapytania trwają 100, 300 i 200 ms. Ile potrwa Promise.all z nimi i w jakiej kolejności będą wyniki?
Promise w interfejsie: sekwencja animacji
Obietnice nie służą tylko do sieci. Web Animations API (element.animate(...)) zwraca animację z obietnicą finished – dzięki temu kroki animacji łańcuchujesz zamiast zagnieżdżać setTimeout:
<div class="scena">
<div id="a" class="kwadrat">1</div>
<div id="b" class="kwadrat">2</div>
<div id="c" class="kwadrat">3</div>
</div>
<button id="start" type="button">Start</button>
<label><input type="checkbox" id="rownolegle"> równolegle (Promise.all)</label>
<p id="status"></p>.scena { position: relative; height: 70px; }
.kwadrat { position: absolute; left: 0; width: 56px; height: 56px; display: grid; place-items: center; color: #fff; font-weight: 700; border-radius: 10px; }
#a { top: 0; background: #2563eb; } #b { top: 0; left: 70px; background: #dc2626; } #c { top: 0; left: 140px; background: #16a34a; }const kwadraty = ['a', 'b', 'c'].map((id) => document.getElementById(id));
const status = document.getElementById('status');
const btn = document.getElementById('start');
// animuj(el, px) zwraca obietnicę spełnioną, gdy animacja się skończy
function animuj(el, px) {
const anim = el.animate([{ transform: 'translateX(0)' }, { transform: `translateX(${px}px)` }], {
duration: 600,
easing: 'ease-in-out',
fill: 'forwards',
});
return anim.finished; // obietnica spełniana obiektem animacji
}
const czekaj = (ms) => new Promise((r) => setTimeout(r, ms));
btn.addEventListener('click', () => {
btn.disabled = true;
kwadraty.forEach((k) => k.getAnimations().forEach((a) => a.cancel())); // reset
const start = performance.now();
status.textContent = 'animuję…';
const rownolegle = document.getElementById('rownolegle').checked;
const gotowe = rownolegle
? Promise.all(kwadraty.map((k) => animuj(k, 160))) // wszystkie naraz
: animuj(kwadraty[0], 160) // jedna po drugiej – łańcuch
.then(() => animuj(kwadraty[1], 160))
.then(() => animuj(kwadraty[2], 160));
gotowe
.then(() => czekaj(300))
.then(() => {
status.textContent = `gotowe po ${Math.round(performance.now() - start)} ms`;
})
.catch((e) => {
status.textContent = 'anulowano: ' + e.name; // cancel() odrzuca finished z AbortError
})
.finally(() => {
btn.disabled = false;
});
});Tryb sekwencyjny trwa ok. 3 × 600 + 300 ms, równoległy – 600 + 300 ms. Tak samo będzie z zapytaniami do serwera w następnej lekcji.
Ściąga
| Składnia | Co robi | Przykład |
|---|---|---|
| stos → mikrozadania → render → 1 zadanie | kolejność pętli zdarzeń | log, then, setTimeout → sync, then, timer |
setTimeout(f, 0) | zadanie (makro) – po mikrozadaniach; render może nastąpić przed nim | „zrób później, w osobnym zadaniu” |
queueMicrotask(f), .then(f) | mikrozadanie – przed renderem | „zrób po bieżącym kodzie” |
new Promise((resolve, reject) => …) | tworzy obietnicę; executor działa synchronicznie | czekaj(ms) |
resolve(x) / reject(e) | spełnia / odrzuca – raz; reject z obiektem Error | reject(new Error('…')) |
.then(f) | po sukcesie; zwraca nową obietnicę z tym, co f zwróci | return pobierz(id) – rozpakowane |
.catch(f) | łapie odrzucenie i wyjątki z wcześniejszych then | na końcu każdego łańcucha |
.finally(f) | zawsze; bez argumentu; przepuszcza wartość | ukryj spinner |
Promise.resolve(x) / Promise.reject(e) | gotowa obietnica | testy, wartości domyślne |
Promise.all([…]) | wszystkie albo pierwszy błąd; wyniki w kolejności wejścia | kilka niezbędnych zapytań |
Promise.allSettled([…]) | raport { status, value/reason } | „co się udało” |
Promise.race([…]) | pierwsza rozstrzygnięta | timeout |
Promise.any([…]) | pierwsza spełniona; AggregateError, gdy żadna | serwery zapasowe |
Promise.withResolvers() | { promise, resolve, reject } do użycia poza executorem | czekanie na zdarzenie |
el.animate(…).finished | obietnica końca animacji | sekwencje animacji |
Zadania
Odliczanie na obietnicach
Napisz funkcję czekaj(ms) zwracającą obietnicę i użyj jej, by wypisać w konsoli 3, 2, 1, Start! w odstępach 300 ms – łańcuchem then, bez zagnieżdżania i bez setInterval. Potem przerób to na funkcję odliczaj(od), która dla dowolnego od buduje łańcuch w pętli (for dokładający kolejne then do zmiennej) i zwraca obietnicę spełnioną po „Start!”.
Pokaż rozwiązanie
const czekaj = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// wersja 1: łańcuch na sztywno
czekaj(0)
.then(() => console.log(3))
.then(() => czekaj(300))
.then(() => console.log(2))
.then(() => czekaj(300))
.then(() => console.log(1))
.then(() => czekaj(300))
.then(() => console.log('Start!'));
// wersja 2: łańcuch budowany w pętli – dla dowolnej liczby kroków
function odliczaj(od, odstep = 100) {
let lancuch = Promise.resolve(); // punkt startowy – już spełniona obietnica
for (let i = od; i >= 1; i--) {
lancuch = lancuch.then(() => console.log('odliczaj:', i)).then(() => czekaj(odstep));
}
return lancuch.then(() => console.log('odliczaj: Start!'));
}
odliczaj(3).then(() => console.log('odliczaj zakończone'));Promisyfikacja i obsługa błędów
Dana jest „stara” funkcja sprawdzLogin(login, callback), która po 150 ms wywołuje callback(blad, wynik): dla loginu krótszego niż 3 znaki – blad = new Error('Login za krótki'), dla loginu admin – blad = new Error('Login zajęty'), w innym wypadku wynik = { login, dostepny: true }. Napisz sprawdzLoginP(login) zwracającą obietnicę i użyj jej dla trzech loginów: ab, admin, ola2026, wypisując dla każdego OK: ola2026 albo Błąd (ab): Login za krótki. Na końcu (po wszystkich trzech, użyj Promise.allSettled) wypisz Sprawdzono 3 loginy, dostępne: 1.
Pokaż rozwiązanie
function sprawdzLogin(login, callback) {
setTimeout(() => {
if (login.length < 3) return callback(new Error('Login za krótki'));
if (login === 'admin') return callback(new Error('Login zajęty'));
callback(null, { login, dostepny: true });
}, 150);
}
// opakowanie: callback (blad, wynik) → resolve/reject
function sprawdzLoginP(login) {
return new Promise((resolve, reject) => {
sprawdzLogin(login, (blad, wynik) => (blad ? reject(blad) : resolve(wynik)));
});
}
const loginy = ['ab', 'admin', 'ola2026'];
// każdy login sprawdzamy JEDEN raz – te same obietnice posłużą do komunikatów i do podsumowania
const obietnice = loginy.map(sprawdzLoginP);
// każda obietnica ma własny catch, więc żadna nie „wysypie” pozostałych
obietnice.forEach((obietnica, i) => {
obietnica
.then((w) => console.log('OK:', w.login))
.catch((e) => console.log(`Błąd (${loginy[i]}): ${e.message}`));
});
// allSettled na ORYGINALNYCH obietnicach (bez catch), żeby odczytać statusy
Promise.allSettled(obietnice).then((wyniki) => {
const dostepne = wyniki.filter((w) => w.status === 'fulfilled').length;
console.log(`Sprawdzono ${wyniki.length} loginy, dostępne: ${dostepne}`);
});Ładowanie z paskiem postępu
Zasymuluj ładowanie trzech zasobów (czekaj z losowym czasem 300–1000 ms, nazwy: „grafika”, „dane”, „czcionki”). Po kliknięciu „Ładuj”: wyłącz przycisk, pokaż listę zasobów ze statusem „…”, a każdy ukończony oznacz „✓” i zwiększ pasek postępu (<progress max="3">). Gdy wszystkie się skończą (Promise.all), wypisz „Gotowe w X ms” i włącz przycisk. Dodatkowo checkbox „symuluj błąd”: wtedy zasób „dane” zostaje odrzucony po 500 ms – pokaż „✗” przy nim, komunikat błędu i mimo błędu włącz przycisk (finally).
Pokaż rozwiązanie
<button id="laduj" type="button">Ładuj</button>
<label><input type="checkbox" id="blad"> symuluj błąd</label>
<progress id="postep" max="3" value="0"></progress>
<ul id="lista"></ul>
<p id="status"></p>progress { display: block; width: 100%; margin: 8px 0; }
ul { margin: 0; padding-left: 20px; }const ZASOBY = ['grafika', 'dane', 'czcionki'];
const btn = document.getElementById('laduj');
const postep = document.getElementById('postep');
const lista = document.getElementById('lista');
const status = document.getElementById('status');
const czekaj = (ms) => new Promise((r) => setTimeout(r, ms));
const losowo = (min, max) => min + Math.random() * (max - min);
function ladujZasob(nazwa, zBledem) {
if (zBledem && nazwa === 'dane') {
return czekaj(500).then(() => {
throw new Error('Serwer danych nie odpowiada');
});
}
return czekaj(losowo(300, 1000)).then(() => nazwa);
}
btn.addEventListener('click', () => {
btn.disabled = true;
postep.value = 0;
status.textContent = '';
lista.innerHTML = ZASOBY.map((z) => `<li data-zasob="${z}">${z}: …</li>`).join('');
const zBledem = document.getElementById('blad').checked;
const start = performance.now();
const obietnice = ZASOBY.map((nazwa) =>
ladujZasob(nazwa, zBledem).then(
(w) => {
lista.querySelector(`[data-zasob="${nazwa}"]`).textContent = `${nazwa}: ✓`;
postep.value++;
return w;
},
(e) => {
lista.querySelector(`[data-zasob="${nazwa}"]`).textContent = `${nazwa}: ✗`;
throw e; // przekazujemy błąd dalej, żeby Promise.all go zobaczył
}
)
);
Promise.all(obietnice)
.then(() => {
status.textContent = `Gotowe w ${Math.round(performance.now() - start)} ms`;
})
.catch((e) => {
status.textContent = 'Błąd: ' + e.message;
})
.finally(() => {
btn.disabled = false; // zawsze – inaczej po błędzie przycisk zostałby wyłączony
});
});Timeout i ponawianie
Napisz dwie funkcje narzędziowe i przetestuj je na symulowanym, zawodnym serwerze:
zLimitem(obietnica, ms)– zwraca obietnicę, która odrzuca zError('Przekroczono czas'), jeśliobietnicanie rozstrzygnie się wms(użyjPromise.race); timer po wygranej obietnicy wyczyść, żeby nie wisiał,sprobuj(fn, proby, odstep)– wywołujefn()(funkcję zwracającą obietnicę); przy odrzuceniu czekaodstepms i próbuje ponownie, maksymalnieprobyrazy; wypisuje numer próby; po wyczerpaniu prób odrzuca z ostatnim błędem.
Serwer: zawodnyServer() – w 2 pierwszych wywołaniach odrzuca po 100 ms (Error('503')), od trzeciego spełnia po 100 ms wartością 'dane'. Pokaż: sprobuj(() => zLimitem(zawodnyServer(), 400), 4, 100) → sukces w 3. próbie; oraz zLimitem(czekaj(900), 300) → „Przekroczono czas”.
Pokaż rozwiązanie
const czekaj = (ms, w) => new Promise((r) => setTimeout(() => r(w), ms));
function zLimitem(obietnica, ms) {
let timer;
const limit = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Przekroczono czas')), ms);
});
// kto pierwszy: wynik czy limit; finally sprząta timer w obu przypadkach
return Promise.race([obietnica, limit]).finally(() => clearTimeout(timer));
}
function sprobuj(fn, proby, odstep) {
let numer = 0;
const jednaProba = () => {
numer++;
return fn().catch((blad) => {
console.log(`próba ${numer} nieudana: ${blad.message}`);
if (numer >= proby) throw blad; // koniec – oddaj ostatni błąd
return czekaj(odstep).then(jednaProba); // rekurencja na obietnicach
});
};
return jednaProba();
}
// zawodny serwer: dwa pierwsze wywołania padają
let wywolania = 0;
function zawodnyServer() {
wywolania++;
return wywolania <= 2
? czekaj(100).then(() => {
throw new Error('503');
})
: czekaj(100, 'dane');
}
sprobuj(() => zLimitem(zawodnyServer(), 400), 4, 100).then((w) => console.log('sukces:', w, `(po ${wywolania} wywołaniach)`));
zLimitem(czekaj(900, 'za późno'), 300).catch((e) => console.log('limit:', e.message));Test powtórkowy
Test powtórkowy
Kolejność wykonania, stany obietnic, łańcuchy i Promise.all. Jedna poprawna odpowiedź.
Co wypisze ten kod?
console.log('a');
setTimeout(() => console.log('b'));
Promise.resolve().then(() => console.log('c'));
console.log('d');Które zdanie o pętli zdarzeń jest prawdziwe?
Ile razy wykona się console.log?
const p = new Promise((resolve, reject) => {
resolve('ok');
reject(new Error('nie'));
resolve('znowu');
});
p.then((w) => console.log(w)).catch((e) => console.log(e.message));Co wypisze ostatni then?
Promise.resolve(5)
.then((x) => x + 1)
.then((x) => Promise.resolve(x * 2))
.then((x) => console.log(x));Gdzie trafi błąd rzucony w drugim then?
pobierz()
.then(a) // 1
.then(() => { throw new Error('x'); }) // 2
.then(c) // 3
.catch(d) // 4
.then(e); // 5Co wypisze ten kod?
Promise.resolve(1)
.finally(() => 100)
.then((x) => console.log(x));Która metoda zwróci wyniki wszystkich obietnic, także gdy część z nich zostanie odrzucona?
Co wypisze ten kod?
const p = new Promise((resolve) => {
console.log('executor');
setTimeout(() => resolve('gotowe'), 0);
});
console.log('po new Promise');
p.then(console.log);Dlaczego poniższy kod wypisuje undefined?
pobierzUzytkownika(1)
.then((u) => { pobierzZamowienia(u.id); })
.then((z) => console.log(z));Jaki wynik da Promise.race([czekaj(300, 'a'), czekaj(100, 'b'), czekaj(200, 'c')])?