Lekce 0.7 · JavaScript obecně
Asynchronní JavaScript
setTimeout a event loop, Promise, async/await, chyby, Promise.all a fetch.
Načítám lekci…
Lekce 0.7 · JavaScript obecně
setTimeout a event loop, Promise, async/await, chyby, Promise.all a fetch.
Načítám lekci…
Představ si restauraci s jediným číšníkem. Kdyby po každé objednávce stál u kuchyně a čekal, až kuchař dovaří, ostatní hosté by se ani nedostali k objednání. Proto to dělá chytře: objednávku odnese do kuchyně, hostovi nechá lísteček („jídlo bude“) a jde obsloužit další stůl. Když kuchař zazvoní, jídlo donese.
JavaScript v prohlížeči je takový číšník – má jen jedno vlákno. Kdyby čekal na odpověď serveru (která trvá klidně sekundu), celá stránka by zamrzla: nešlo by klikat, psát ani scrollovat. Proto se na pomalé věci nečeká, ale objednávají – a lísteček s budoucím výsledkem se jmenuje Promise.
console.log('A')setTimeout(() => console.log('B – po 1 sekundě'), 1000)console.log('C')// Vypíše: A, C, … a za sekundu B// setTimeout nečeká! Jen „objedná“ zavolání funkce a kód běží dál.const id = setTimeout(() => alert('Bum'), 5000)clearTimeout(id) // zruší naplánované zavoláníconst tick = setInterval(() => console.log('tik'), 1000) // každou sekunduclearInterval(tick) // zastaví
Číšník má pravidla: nejdřív dodělá, co právě dělá (synchronní kód). Pak se podívá na naléhavé lístečky (mikroúlohy – splněné Promise). Teprve potom vezme další běžnou úlohu z fronty (setTimeout, kliknutí, odpověď serveru).
console.log('1')setTimeout(() => console.log('4'), 0) // i s nulou až po všem ostatnímPromise.resolve().then(() => console.log('3')) // mikroúloha – před timeoutemconsole.log('2')// 1, 2, 3, 4
Co vidíš: Kód s console.log, setTimeout a Promise. Tlačítko ho spustí a ukáže, v jakém pořadí se výpisy objevily.
Vyzkoušej:
Co z toho plyne: setTimeout(fn, 0) neznamená „hned“, ale „až bude volno“. Synchronní kód vždy doběhne celý, než se na řadu dostane cokoli asynchronního.
Načítám interaktivní část…
Promise je objekt, který zastupuje hodnotu, jež zatím není k dispozici. Může být ve třech stavech:
| Stav | Znamená | Lísteček v restauraci |
|---|---|---|
pending | čeká se | jídlo se vaří |
fulfilled | splněno – máme hodnotu | jídlo je na stole |
rejected | zamítnuto – nastala chyba | došly suroviny |
const promise = fetchUser(1) // vrátí Promise hned, data ještě nejsouconsole.log(promise) // Promise { <pending> }// .then – „až to bude, udělej tohle“promise.then((user) => console.log(user.name))// .catch – „když se to pokazí“, .finally – „v každém případě“fetchUser(99).then((user) => console.log(user.name)).catch((error) => console.error('Nepovedlo se:', error.message)).finally(() => console.log('Hotovo (úspěch i chyba)'))
fetchUser(1).then((user) => fetchPosts(user.id)) // vrátí další Promise….then((posts) => posts.items.length) // …a další then dostane jeho výsledek.then((count) => console.log(count)).catch((error) => console.error(error)) // jeden catch pro chyby z celého řetězu
Většinou Promise jen dostáváš (z fetch, z knihoven). Občas se ale hodí vyrobit vlastní – typicky čekání:
function sleep(ms) {return new Promise((resolve) => {setTimeout(resolve, ms) // za ms zavolá resolve → Promise se splní})}sleep(1000).then(() => console.log('Uplynula sekunda'))// Promise, který může selhatfunction rollDice() {return new Promise((resolve, reject) => {const n = Math.ceil(Math.random() * 6)if (n === 1) reject(new Error('Padla jednička'))else resolve(n)})}
Řetězy .then fungují, ale čtou se ztěžka. async/await dělá totéž a čte se jako obyčejný kód shora dolů:
// S thenfunction showUserPosts(id) {return fetchUser(id).then((user) => fetchPosts(user.id)).then((posts) => console.log(posts.items))}// S async/await – totéžasync function showUserPosts(id) {const user = await fetchUser(id) // počkej na uživateleconst posts = await fetchPosts(user.id) // pak na příspěvkyconsole.log(posts.items)}
await „rozbalí“ Promise: počká, až se splní, a vrátí jeho hodnotu. Čeká jen tahle funkce – zbytek stránky běží dál.await jde použít jen uvnitř funkce označené async (nebo na nejvyšší úrovni modulu).async funkce vždy vrací Promise. I když napíšeš return 5, volající dostane Promise s pětkou.async function getNumber() {return 5}getNumber() // Promise { 5 } – ne 5!const n = await getNumber() // 5getNumber().then((n) => …) // nebo takhle// Šipková async funkceconst loadUser = async (id) => {const user = await fetchUser(id)return user.name}
async function loadProfile(id) {try {const user = await fetchUser(id) // když tohle selže…return user} catch (error) {console.error(error.message) // …skočí se semreturn null} finally {console.log('Načítání skončilo') // proběhne vždycky (úspěch i chyba)}}
Když potřebuješ víc nezávislých věcí, je zbytečné čekat na každou zvlášť. Číšník také neodnáší do kuchyně objednávky jednu po druhé a nečeká, až se každá dovaří.
// ❌ Postupně: 4 × ~1 s = ~4 sconst users = []for (const id of [1, 2, 3, 4]) {users.push(await fetchUser(id))}// ✅ Najednou: ~1 s (tak dlouho, jak trvá nejpomalejší)const users = await Promise.all([1, 2, 3, 4].map((id) => fetchUser(id)))// ↑ map spustí všechna 4 načítání hned, all počká na všechnaconst [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]) // různé věci najednou
Co vidíš: Dvě tlačítka, která načtou čtyři uživatele z falešného API (každý odpovídá za 0,3–1,5 s). Pruhy ukazují, kdy které načítání začalo a skončilo.
Vyzkoušej:
Co z toho plyne: Nezávislá načítání spouštěj najednou přes Promise.all. Postupně jen tehdy, když další krok potřebuje výsledek předchozího.
Načítám interaktivní část…
| Metoda | Splní se, když… | Selže, když… |
|---|---|---|
Promise.all | se splní všechny (výsledky v pořadí) | selže kterýkoli (hned) |
Promise.allSettled | doběhnou všechny – úspěšně i neúspěšně | nikdy |
Promise.race | první doběhne úspěšně | první doběhne chybou |
Promise.any | kterýkoli se splní | selžou všechny |
// allSettled – když nechceš, aby jedna chyba shodila všechnoconst results = await Promise.allSettled([fetchUser(1), fetchUser(99)])// [{ status: 'fulfilled', value: {…} }, { status: 'rejected', reason: Error }]const found = results.filter((r) => r.status === 'fulfilled').map((r) => r.value)// race – časový limitconst timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Vypršel čas')), 3000))const user = await Promise.race([fetchUser(1), timeout])
V kurzu používáme falešné API (src/course/fakeApi.ts), aby vše fungovalo offline. Ve skutečné aplikaci se se serverem mluví přes vestavěnou funkci fetch:
async function loadUsers() {const response = await fetch('https://api.example.com/users')if (!response.ok) { // ⚠️ fetch NESELŽE při 404 nebo 500!throw new Error(`Server vrátil ${response.status}`)}const users = await response.json() // tělo odpovědi → JS objekt (taky Promise)return users}// Odeslání datawait fetch('/api/posts', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ title: 'Ahoj' }), // objekt → text})
const controller = new AbortController()fetch('/api/search?q=react', { signal: controller.signal }).then((r) => r.json()).catch((e) => {if (e.name === 'AbortError') return // zrušili jsme sami – není to chybathrow e})controller.abort() // zruší probíhající požadavek
Hodí se třeba u vyhledávání: když uživatel píše dál, starý požadavek už nikoho nezajímá. V Reactu se tak ruší požadavky v úklidu efektu (lekce 3).
// ❌ Zapomenutý await – máš Promise, ne dataconst user = fetchUser(1)user.name // undefined – Promise žádné name nemá// ❌ await v forEach nečeká – forEach o Promise nevíids.forEach(async (id) => {await save(id)})console.log('Hotovo') // vypíše se HNED, ukládání ještě běží// ✅ for…of (postupně) nebo Promise.all (najednou)for (const id of ids) await save(id)await Promise.all(ids.map((id) => save(id)))// ❌ map s async vrátí pole Promiseconst names = ids.map(async (id) => (await fetchUser(id)).name) // [Promise, Promise…]// ✅ obal do Promise.allconst names = await Promise.all(ids.map(async (id) => (await fetchUser(id)).name))// ❌ Chyba bez catchasync function load() {const data = await fetchUser(99) // vyhodí chybu a nikdo ji nechytí}
Dvě krátké async funkce: jedna chvíli počká, druhá načte uživatele z falešného API.
VÝZVA: Počkej a odpověz 1. getAnswer() je async funkce: nejdřív počkej 100 ms (await sleep(100)), pak vrať 42. 2. loadCity(1) má načíst uživatele (await fetchUser(id)) a vrátit jeho město – 'Praha'.
Načítám interaktivní část…
Šest funkcí nad falešným API: čekání, načtení jednoho i víc uživatelů najednou, ošetření chyby a časový limit. Testy chvíli trvají, protože API odpovídá se zpožděním.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
try/catch; finally proběhne vždy.Promise.all. await ve forEach nefunguje.fetch neselže při 404/500 – kontroluj response.ok. Rušit jde přes AbortController.Checks, Check, fetchUser, sleep)Checks, Check – ze souboru src/course/checks.tsx. Pomocník kurzu pro výzvy s funkcemi: spustí testy a ukáže ✅/❌. Jak funguje uvnitř, nemusíš řešit – stačí, že ti řekne, které kroky už máš hotové./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}
fetchUser, sleep – ze souboru src/course/fakeApi.ts. Falešné API kurzu: data a funkce, které předstírají server (se zpožděním, občas i s chybou). Ve skutečné aplikaci by tu bylo volání fetch().export interface User {id: numbername: stringemail: stringcity: string}export async function fetchUser(id: number): Promise<User> {await sleep(randomDelay())const user = users.find((u) => u.id === id)if (!user) throw new Error(`Uživatel ${id} neexistuje`)return user}export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))