JavaScript 07 · JavaScript obecně
Asynchronní JavaScript
setTimeout a event loop, Promise, async/await, chyby, Promise.all a fetch.
Načítám lekci…
JavaScript 07 · 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 React 03).
// ❌ 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.
Zadání najdeš v komentářích v kódu níže – na začátku souboru a u míst označených TODO.
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. Co přesně má která vracet, je v komentářích v kódu. Kontrola pod editorem chvíli trvá, protože API odpovídá se zpožděním.
Střední a těžké výzvy se odemknou, jakmile se přihlásíš. Stejně jako celý kurz jsou zdarma – stačí e-mail, žádné heslo ani platba.
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.fetchUser, sleep)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))