Abychom demonstrovali použità callbacků, pÅÃslibů a jiných abstraktnÃch konceptů, budeme použÃvat nÄkteré metody prohlÞeÄe: jmenovitÄ naÄÃtánà skriptů a provádÄnà jednoduchých manipulacà s dokumentem.
Jestliže tyto metody neznáte a jejich použÃvánà v pÅÃkladech je pro vás matoucÃ, možná si budete chtÃt pÅeÄÃst nÄkolik kapitol z dalšà Äásti tutoriálu.
SnažÃme se vÅ¡ak, aby vÅ¡echno bylo jasné. Z prohlÞeÄe tady nebude nic pÅÃliÅ¡ složitého.
Hostitelská prostÅedà JavaScriptu poskytujà mnoho funkcÃ, které nám umožÅujà naplánovat asynchronnà akce. Jinými slovy akce, které spustÃme nynÃ, ale dokonÄà se až pozdÄji.
NapÅÃklad jedna taková funkce je setTimeout.
Ze skuteÄného svÄta existujà i jiné pÅÃklady asynchronnÃch akcÃ, napÅ. naÄÃtánà skriptů a modulů (vysvÄtlÃme je v dalÅ¡Ãch kapitolách).
PodÃvejme se na funkci naÄtiSkript(zdroj), která naÄte skript ze zadaného zdroje zdroj:
function naÄtiSkript(zdroj) {
// vytvoÅà znaÄku <script> a pÅipojà ji na stránku
// to způsobÃ, že se skript zaÄne naÄÃtat ze zadaného zdroje, a až bude kompletnÃ, spustà se
let skript = document.createElement('script');
skript.src = zdroj;
document.head.append(skript);
}
Funkce vložà do dokumentu novou, dynamicky vytvoÅenou znaÄku <script src="â¦"> se zadaným zdrojem zdroj v src. ProhlÞeÄ jej zaÄne automaticky naÄÃtat, a až skript bude kompletnÃ, spustà jej.
Tuto funkci můžeme použÃvat následovnÄ:
// naÄte skript ze zadané cesty a spustà ho
naÄtiSkript('/moje/skript.js');
Skript se spustà âasynchronnÄâ, zaÄne se tedy naÄÃtat okamžitÄ, ale spustà se až pozdÄji, když už funkce skonÄila.
Pokud je za naÄtiSkript(â¦) nÄjaký kód, nebude Äekat, než naÄÃtánà skriptu skonÄÃ.
naÄtiSkript('/moje/skript.js');
// kód za funkcà naÄtiSkript
// nepoÄká, než naÄÃtánà skriptu skonÄÃ
// ...
ÅeknÄme, že skript potÅebujeme použÃt hned, jakmile se naÄte. Skript deklaruje nové funkce a my je chceme spustit.
Pokud to vÅ¡ak udÄláme ihned po volánà naÄtiSkript(â¦), nebude to fungovat:
naÄtiSkript('/moje/skript.js'); // skript obsahuje "function nováFunkce() {â¦}"
nováFunkce(); // taková funkce neexistuje!
PochopitelnÄ, prohlÞeÄ pravdÄpodobnÄ nemÄl dost Äasu na naÄtenà skriptu. Funkce naÄtiSkript nám tedy zatÃm neposkytuje způsob, jak vystopovat dokonÄenà naÄÃtánÃ. Skript se naÄte a nakonec se spustÃ, to je vÅ¡e. My bychom vÅ¡ak rádi vÄdÄli, kdy se to stane, abychom ze skriptu mohli použÃvat nové funkce a promÄnné.
PÅidejme jako druhý argument funkce naÄtiSkript funkci callback, která by se mÄla spustit, až se skript naÄte:
function naÄtiSkript(zdroj, callback) {
let skript = document.createElement('script');
skript.src = zdroj;
skript.onload = () => callback(skript);
document.head.append(skript);
}
Událost onload je popsána v Älánku NaÄÃtánà zdrojů: onload a onerror. V zásadÄ spustà funkci poté, co je skript naÄten a proveden.
Když nynà chceme volat nové funkce ze skriptu, mÄli bychom to uvést v callbacku:
naÄtiSkript('/moje/skript.js', function() {
// callback se spustà po naÄtenà skriptu
nováFunkce(); // takže nynà to funguje
...
});
MyÅ¡lenka je taková: druhý argument je funkce (zpravidla anonymnÃ), která se spustÃ, až bude akce dokonÄena.
Zde je spustitelný pÅÃklad se skuteÄným skriptem:
function naÄtiSkript(zdroj, callback) {
let skript = document.createElement('script');
skript.src = zdroj;
skript.onload = () => callback(skript);
document.head.append(skript);
}
naÄtiSkript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', skript => {
alert(`Hurá, skript ${skript.src} se naÄetl`);
alert( _ ); // _ je funkce deklarovaná v naÄteném skriptu
});
Tento styl asynchronnÃho programovánà se nazývá âzaložený na callbackuâ (âcallback-basedâ). Funkce, která nÄco provádà asynchronnÄ, by mÄla poskytovat argument callback, do nÄhož pÅedáme funkci, která se má spustit, až bude původnà funkce hotová.
Zde jsme to udÄlali ve funkci naÄtiSkript, ale samozÅejmÄ je to obecný pÅÃstup.
Callback v callbacku
Jak můžeme naÄÃst dva skripty za sebou: napÅed prvnà a po nÄm druhý?
PÅirozené ÅeÅ¡enà by bylo umÃstit druhé volánà funkce naÄtiSkript do callbacku, napÅÃklad:
naÄtiSkript('/moje/skript.js', function(skript) {
alert(`Hurá, ${skript.src} se naÄetl, teÄ naÄteme dalÅ¡Ã`);
naÄtiSkript('/my/script2.js', function(skript) {
alert(`Hurá, druhý skript se naÄetl`);
});
});
Poté, co skonÄà vnÄjšà funkce naÄtiSkript, callback vyvolá vnitÅnÃ.
Co kdybychom chtÄli jeÅ¡tÄ dalšà skript?
naÄtiSkript('/moje/skript.js', function(skript) {
naÄtiSkript('/my/script2.js', function(skript) {
naÄtiSkript('/my/script3.js', function(skript) {
// ...pokraÄujeme, dokud se nenaÄtou vÅ¡echny skripty
});
});
});
Každá nová akce je tedy uvnitÅ callbacku. To je vhodné pro nÄkolik akcÃ, ale ne pro vÄtšà poÄet. Brzy tedy uvidÃme dalšà varianty.
OÅ¡etÅovánà chyb
V uvedených pÅÃkladech jsme nebrali v úvahu chyby. Co když naÄÃtánà skriptu selže? Náš callback by mÄl být schopen na to reagovat.
Zde je vylepÅ¡ená verze funkce naÄtiSkript, která stopuje chyby pÅi naÄÃtánÃ:
function naÄtiSkript(zdroj, callback) {
let skript = document.createElement('script');
skript.src = zdroj;
skript.onload = () => callback(null, skript);
skript.onerror = () => callback(new Error(`Chyba naÄÃtánà skriptu pro ${zdroj}`));
document.head.append(skript);
}
Funkce volá callback(null, skript) pÅi úspÄÅ¡ném naÄtenà a callback(chyba) jinak.
PoužitÃ:
naÄtiSkript('/moje/skript.js', function(chyba, skript) {
if (chyba) {
// oÅ¡etÅenà chyby
} else {
// skript úspÄÅ¡nÄ naÄten
}
});
OpÄt jsme pro funkci naÄtiSkript použili způsob, jaký je ve skuteÄnosti pomÄrnÄ bÄžný. Nazývá se styl âcallbacku s chybou na prvnÃm mÃstÄâ (âerror-first callbackâ).
Konvence je:
- Prvnà argument funkce
callbackje rezervován pro chybu, pokud nÄjaká nastane. Pak se volácallback(chyba). - Druhý argument (a pÅÃpadnÄ dalÅ¡Ã, jsou-li zapotÅebÃ) je pro úspÄÅ¡ný výsledek. Pak se volá
callback(null, výsledek1, výsledek2â¦).
Jediná funkce callback se tedy použÃvá jak pro hlášenà chyb, tak pro dalšà pÅedánà výsledků.
Pyramida zkázy
Na prvnà pohled to vypadá jako životaschopný pÅÃstup k asynchronnÃmu kódovánÃ. A nepochybnÄ jÃm také je. Pro jedno nebo možná dvÄ vnoÅená volánà to vypadá dobÅe.
AvÅ¡ak pro vÃcenásobné asynchronnà akce, které následujà jedna po druhé, budeme mÃt kód podobný tomuto:
naÄtiSkript('1.js', function(chyba, skript) {
if (chyba) {
oÅ¡etÅiChybu(chyba);
} else {
// ...
naÄtiSkript('2.js', function(chyba, skript) {
if (chyba) {
oÅ¡etÅiChybu(chyba);
} else {
// ...
naÄtiSkript('3.js', function(chyba, skript) {
if (chyba) {
oÅ¡etÅiChybu(chyba);
} else {
// ...pokraÄujeme, dokud se nenaÄtou vÅ¡echny skripty (*)
}
});
}
});
}
});
V uvedeném kódu:
- NaÄteme
1.js, pak pokud nenastala chyba⦠- NaÄteme
2.js, pak pokud nenastala chyba⦠- NaÄteme
3.js, pak pokud nenastala chyba â udÄláme nÄco jiného(*).
Když se volánà budou stále vnoÅovat, kód bude stále hlubšà a jeho údržba ÄÃm dál obtÞnÄjÅ¡Ã, zvláštÄ pokud mÃsto ... máme skuteÄný kód, který může obsahovat dalšà cykly, podmÃnÄné pÅÃkazy a podobnÄ.
Tomu se nÄkdy ÅÃká âcallbackové pekloâ nebo âpyramida zkázyâ.
S každou dalšà asynchronnà akcà se tato âpyramidaâ vnoÅených volánà bude protahovat smÄrem doprava a brzy se vymkne kontrole.
Tento způsob kódovánà tedy nenà pÅÃliÅ¡ vhodný.
Můžeme se tento problém pokusit utlumit tÃm, že vložÃme každou akci do samostatné funkce, napÅÃklad:
naÄtiSkript('1.js', krok1);
function krok1(chyba, skript) {
if (chyba) {
oÅ¡etÅiChybu(chyba);
} else {
// ...
naÄtiSkript('2.js', krok2);
}
}
function krok2(chyba, skript) {
if (chyba) {
oÅ¡etÅiChybu(chyba);
} else {
// ...
naÄtiSkript('3.js', krok3);
}
}
function krok3(chyba, skript) {
if (chyba) {
oÅ¡etÅiChybu(chyba);
} else {
// ...pokraÄujeme, dokud se nenaÄtou vÅ¡echny skripty (*)
}
}
VidÃte? DÄlá totéž a neobsahuje žádné hluboké vnoÅovánÃ, protože jsme každou akci umÃstili do oddÄlené funkce na nejvyššà úrovni.
Funguje to, ale kód nynà vypadá jako roztrhané prostÄradlo. Å patnÄ se Äte a pravdÄpodobnÄ jste si vÅ¡imli, že ÄlovÄk musà pÅi jeho Ätenà pÅeskakovat oÄima mezi jednotlivými Äástmi. To je nepohodlné, zvláštÄ pokud ÄtenáŠtento kód nezná a nevÃ, kam se má podÃvat.
NavÃc vÅ¡echny funkce nazvané krok* jsou na jedno použitÃ, byly vytvoÅeny jen proto, abychom se vyhnuli âpyramidÄ zkázyâ. Mimo ÅetÄzec akcà je nikdo použÃvat nebude. Zavlékáme tedy trochu nepoÅádku do jmenného prostoru.
ChtÄli bychom mÃt nÄco lepÅ¡Ãho.
NaÅ¡tÄstà existujà i jiné způsoby, jak se takovým pyramidám vyhnout. JednÃm z nejlepÅ¡Ãch je použÃvánà âpÅÃslibůâ, které popÃÅ¡eme v následujÃcà kapitole.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)