Úvod:zpětná volání

Zde v příkladech používáme metody prohlížeče

Abychom demonstrovali použití zpětných volání, slibů a dalších abstraktních pojmů, použijeme některé metody prohlížeče:konkrétně načítání skriptů a provádění jednoduchých manipulací s dokumenty.

Pokud tyto metody neznáte a jejich použití v příkladech je matoucí, možná si budete chtít přečíst několik kapitol z další části tutoriálu.

Přesto se pokusíme věci objasnit. Z hlediska prohlížeče nebude nic skutečně složitého.

Mnoho funkcí poskytuje hostitelská prostředí JavaScriptu, která vám umožňují naplánovat asynchronní akce. Jinými slovy, akce, které zahájíme nyní, ale skončí později.

Jednou z takových funkcí je například setTimeout funkce.

Existují další příklady asynchronních akcí z reálného světa, např. načítání skriptů a modulů (probereme je v dalších kapitolách).

Podívejte se na funkci loadScript(src) , který načte skript s daným src :

function loadScript(src) {
 // creates a <script> tag and append it to the page
 // this causes the script with given src to start loading and run when complete
 let script = document.createElement('script');
 script.src = src;
 document.head.append(script);
}

Vloží do dokumentu nový, dynamicky vytvořený tag <script src="…"> s daným src . Prohlížeč jej automaticky začne načítat a po dokončení se spustí.

Tuto funkci můžeme použít takto:

// load and execute the script at the given path
loadScript('/my/script.js');

Skript se provádí „asynchronně“, protože se začne načítat nyní, ale spustí se později, když je funkce již dokončena.

Pokud je nějaký kód pod loadScript(…) , nečeká na dokončení načítání skriptu.

loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...

Řekněme, že musíme použít nový skript, jakmile se načte. Deklaruje nové funkce a my je chceme spustit.

Ale pokud to uděláme bezprostředně po loadScript(…) zavolejte, to by nefungovalo:

loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

Prohlížeč samozřejmě pravděpodobně neměl čas načíst skript. Od této chvíle loadScript funkce neposkytuje způsob, jak sledovat dokončení načítání. Skript se načte a nakonec se spustí, to je vše. Ale rádi bychom věděli, kdy se to stane, abychom použili nové funkce a proměnné z tohoto skriptu.

Přidejme callback fungovat jako druhý argument k loadScript který by se měl spustit při načtení skriptu:

function loadScript(src, callback) {
 let script = document.createElement('script');
 script.src = src;

 script.onload = () => callback(script);

 document.head.append(script);
}

onload událost je popsána v článku Načítání zdrojů:onload a onerror, v podstatě spustí funkci po načtení a spuštění skriptu.

Nyní, pokud chceme volat nové funkce ze skriptu, měli bychom to napsat do callback:

loadScript('/my/script.js', function() {
 // the callback runs after the script is loaded
 newFunction(); // so now it works
 ...
});

To je myšlenka:druhý argument je funkce (obvykle anonymní), která se spustí po dokončení akce.

Zde je spustitelný příklad se skutečným skriptem:

function loadScript(src, callback) {
 let script = document.createElement('script');
 script.src = src;
 script.onload = () => callback(script);
 document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
 alert(`Cool, the script ${script.src} is loaded`);
 alert( _ ); // _ is a function declared in the loaded script
});

Tomu se říká styl asynchronního programování „založený na zpětném volání“. Funkce, která něco dělá asynchronně, by měla poskytovat callback argument, kam vložíme funkci, aby se spustila po jejím dokončení.

Zde jsme to udělali v loadScript , ale jde samozřejmě o obecný přístup.

Zpětné volání ve zpětném volání

Jak můžeme načíst dva skripty postupně:první a poté druhý?

Přirozeným řešením by bylo vložit druhý loadScript zavolejte v rámci zpětného volání, takto:

loadScript('/my/script.js', function(script) {

 alert(`Cool, the ${script.src} is loaded, let's load one more`);

 loadScript('/my/script2.js', function(script) {
 alert(`Cool, the second script is loaded`);
 });

});

Za vnějším loadScript je dokončeno, zpětné volání zahájí vnitřní.

Co když chceme ještě jeden skript…?

loadScript('/my/script.js', function(script) {

 loadScript('/my/script2.js', function(script) {

 loadScript('/my/script3.js', function(script) {
 // ...continue after all scripts are loaded
 });

 });

});

Takže každá nová akce je uvnitř zpětného volání. To je v pořádku pro několik akcí, ale ne pro mnoho, takže brzy uvidíme další varianty.

Zpracování chyb

Ve výše uvedených příkladech jsme chyby nezohlednili. Co když se načítání skriptu nezdaří? Naše zpětné volání by na to mělo být schopné reagovat.

Zde je vylepšená verze loadScript který sleduje chyby načítání:

function loadScript(src, callback) {
 let script = document.createElement('script');
 script.src = src;

 script.onload = () => callback(null, script);
 script.onerror = () => callback(new Error(`Script load error for ${src}`));

 document.head.append(script);
}

Volá callback(null, script) pro úspěšné načtení a callback(error) jinak.

Použití:

loadScript('/my/script.js', function(error, script) {
 if (error) {
 // handle error
 } else {
 // script loaded successfully
 }
});

Ještě jednou recept, který jsme použili pro loadScript je vlastně docela běžné. Říká se tomu styl „první zpětné volání při chybě“.

Konvence je:

  1. První argument z callback je vyhrazeno pro chybu, pokud k ní dojde. Potom callback(err) se nazývá.
  2. Druhý argument (a další v případě potřeby) slouží k úspěšnému výsledku. Potom callback(null, result1, result2…) se nazývá.

Takže jediný callback funkce se používá jak pro hlášení chyb, tak pro předávání výsledků zpět.

Pyramida zkázy

Na první pohled to vypadá jako životaschopný přístup k asynchronnímu kódování. A skutečně je. Pro jedno nebo možná dvě vnořená volání to vypadá dobře.

Ale pro více asynchronních akcí, které následují jedna po druhé, budeme mít kód takto:

loadScript('1.js', function(error, script) {

 if (error) {
 handleError(error);
 } else {
 // ...
 loadScript('2.js', function(error, script) {
 if (error) {
 handleError(error);
 } else {
 // ...
 loadScript('3.js', function(error, script) {
 if (error) {
 handleError(error);
 } else {
 // ...continue after all scripts are loaded (*)
 }
 });

 }
 });
 }
});

Ve výše uvedeném kódu:

  1. Načítáme 1.js , pak pokud nedojde k chybě…
  2. Načítáme 2.js , pak pokud nedojde k chybě…
  3. Načítáme 3.js , pak pokud nedojde k žádné chybě – udělejte něco jiného (*) .

Jak se volání více vnořují, kód se prohlubuje a je stále obtížnější jej spravovat, zvláště pokud máme skutečný kód namísto ... které mohou zahrnovat více smyček, podmíněné příkazy a tak dále.

Někdy se tomu říká „peklo zpětného volání“ nebo „pyramida zkázy“.

„Pyramida“ vnořených hovorů roste doprava s každou asynchronní akcí. Brzy se to vymkne kontrole.

Takže tento způsob kódování není moc dobrý.

Můžeme se pokusit problém zmírnit tím, že z každé akce uděláme samostatnou funkci, jako je tato:

loadScript('1.js', step1);

function step1(error, script) {
 if (error) {
 handleError(error);
 } else {
 // ...
 loadScript('2.js', step2);
 }
}

function step2(error, script) {
 if (error) {
 handleError(error);
 } else {
 // ...
 loadScript('3.js', step3);
 }
}

function step3(error, script) {
 if (error) {
 handleError(error);
 } else {
 // ...continue after all scripts are loaded (*)
 }
}

Vidět? Dělá to to samé a nyní nedochází k žádnému hlubokému vnořování, protože jsme z každé akce udělali samostatnou funkci nejvyšší úrovně.

Funguje to, ale kód vypadá jako roztrhaná tabulka. Je těžké to číst a pravděpodobně jste si všimli, že při čtení je třeba mezi jednotlivými díly přeskakovat. To je nepohodlné, zvláště pokud čtenář nezná kód a neví, kam skočit.

Také funkce s názvem step* jsou všechny na jedno použití, jsou vytvořeny pouze proto, aby se vyhnuly „pyramidě zkázy“. Nikdo je nebude znovu používat mimo akční řetězec. Takže je tu trochu nepořádek jmenného prostoru.

Rádi bychom měli něco lepšího.

Naštěstí existují i ​​jiné způsoby, jak se takovým pyramidám vyhnout. Jedním z nejlepších způsobů je použít „sliby“, popsané v další kapitole.