Řetězení slibů

Vraťme se k problému uvedenému v kapitole Úvod:zpětná volání:máme sekvenci asynchronních úloh, které je třeba provést jednu po druhé – například načítání skriptů. Jak to můžeme dobře nakódovat?

Sliby poskytují několik receptů, jak toho dosáhnout.

V této kapitole se zabýváme řetězením slibů.

Vypadá to takto:

new Promise(function(resolve, reject) {

 setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

 alert(result); // 1
 return result * 2;

}).then(function(result) { // (***)

 alert(result); // 2
 return result * 2;

}).then(function(result) {

 alert(result); // 4
 return result * 2;

});

Myšlenka je, že výsledek je předán řetězem .then manipulátory.

Zde je postup:

  1. Počáteční příslib se vyřeší za 1 sekundu (*) ,
  2. Poté .then handler se nazývá (**) , což zase vytvoří nový příslib (vyřešený pomocí 2 hodnotu).
  3. Další then (***) získá výsledek předchozího, zpracuje ho (zdvojnásobí) a předá dalšímu handleru.
  4. …a tak dále.

Když je výsledek předán řetězem obslužných rutin, můžeme vidět sekvenci alert volání:124 .

Celá věc funguje, protože každé volání na .then vrátí nový příslib, takže můžeme zavolat další .then na to.

Když handler vrátí hodnotu, stane se výsledkem tohoto slibu, takže dalších .then se s ním nazývá.

Klasická nováčkovská chyba:technicky můžeme také přidat mnoho .then k jedinému slibu. Toto není řetězení.

Například:

let promise = new Promise(function(resolve, reject) {
 setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
 alert(result); // 1
 return result * 2;
});

promise.then(function(result) {
 alert(result); // 1
 return result * 2;
});

promise.then(function(result) {
 alert(result); // 1
 return result * 2;
});

To, co jsme zde udělali, je jen několik manipulátorů na jeden slib. Nepředávají si výsledek; místo toho jej zpracovávají nezávisle.

Zde je obrázek (porovnejte s řetězením výše):

Vše .then na stejném slibu získat stejný výsledek – výsledek tohoto slibu. Takže v kódu především alert zobrazit totéž:1 .

V praxi jen zřídka potřebujeme více zpracovatelů pro jeden slib. Řetězení se používá mnohem častěji.

Vracející se sliby

Obslužný program používaný v .then(handler) může vytvořit a vrátit slib.

V takovém případě další handleři počkají, až se to ustálí, a pak dostanou výsledek.

Například:

new Promise(function(resolve, reject) {

 setTimeout(() => resolve(1), 1000);

}).then(function(result) {

 alert(result); // 1

 return new Promise((resolve, reject) => { // (*)
 setTimeout(() => resolve(result * 2), 1000);
 });

}).then(function(result) { // (**)

 alert(result); // 2

 return new Promise((resolve, reject) => {
 setTimeout(() => resolve(result * 2), 1000);
 });

}).then(function(result) {

 alert(result); // 4

});

Zde je první .then ukazuje 1 a vrátí new Promise(…) v řádku (*) . Po jedné sekundě se to vyřeší a výsledek (argument resolve , tady je to result * 2 ) je předán obsluze druhého .then . Tento obslužný program je na řádku (**) , ukazuje 2 a dělá to samé.

Výstup je tedy stejný jako v předchozím příkladu:1 → 2 → 4, ale nyní se zpožděním 1 sekundy mezi alert hovory.

Vracení slibů nám umožňuje vytvářet řetězce asynchronních akcí.

Příklad:loadScript

Využijme tuto funkci se slíbeným loadScript , definovaný v předchozí kapitole, pro načítání skriptů jeden po druhém v pořadí:

loadScript("/article/promise-chaining/one.js")
 .then(function(script) {
 return loadScript("/article/promise-chaining/two.js");
 })
 .then(function(script) {
 return loadScript("/article/promise-chaining/three.js");
 })
 .then(function(script) {
 // use functions declared in scripts
 // to show that they indeed loaded
 one();
 two();
 three();
 });

Tento kód lze o něco zkrátit pomocí funkcí šipek:

loadScript("/article/promise-chaining/one.js")
 .then(script => loadScript("/article/promise-chaining/two.js"))
 .then(script => loadScript("/article/promise-chaining/three.js"))
 .then(script => {
 // scripts are loaded, we can use functions declared there
 one();
 two();
 three();
 });

Zde každý loadScript volání vrátí příslib a další .then běží, když se to vyřeší. Poté zahájí načítání dalšího skriptu. Skripty se tedy načítají jeden po druhém.

Do řetězce můžeme přidat více asynchronních akcí. Upozorňujeme, že kód je stále „plochý“ – roste dolů, nikoli doprava. Neexistují žádné známky „pyramidy zkázy“.

Technicky bychom mohli přidat .then přímo každému loadScript , takto:

loadScript("/article/promise-chaining/one.js").then(script1 => {
 loadScript("/article/promise-chaining/two.js").then(script2 => {
 loadScript("/article/promise-chaining/three.js").then(script3 => {
 // this function has access to variables script1, script2 and script3
 one();
 two();
 three();
 });
 });
});

Tento kód dělá totéž:načte 3 skripty v sekvenci. Ale „roste doprava“. Máme tedy stejný problém jako se zpětnými voláními.

Lidé, kteří začnou používat sliby, někdy nevědí o řetězení, takže to píší takto. Obecně je preferováno řetězení.

Někdy je v pořádku napsat .then přímo, protože vnořená funkce má přístup k vnějšímu rozsahu. Ve výše uvedeném příkladu má nejvíce vnořené zpětné volání přístup ke všem proměnným script1 , script2 , script3 . Ale to je spíše výjimka než pravidlo.

Thenables

Abychom byli přesní, handler nemusí vrátit přesně slib, ale takzvaný „thenable“ objekt – libovolný objekt, který má metodu .then . Bude s ním nakládáno stejně jako se slibem.

Myšlenka je taková, že knihovny třetích stran mohou implementovat své vlastní objekty „slučitelné se sliby“. Mohou mít rozšířenou sadu metod, ale také být kompatibilní s nativními sliby, protože implementují .then .

Zde je příklad potomovatelného objektu:

class Thenable {
 constructor(num) {
 this.num = num;
 }
 then(resolve, reject) {
 alert(resolve); // function() { native code }
 // resolve with this.num*2 after the 1 second
 setTimeout(() => resolve(this.num * 2), 1000); // (**)
 }
}

new Promise(resolve => resolve(1))
 .then(result => {
 return new Thenable(result); // (*)
 })
 .then(alert); // shows 2 after 1000ms

JavaScript kontroluje objekt vrácený .then handler na řádku (*) :pokud má volatelnou metodu s názvem then , pak zavolá metodu poskytující nativní funkce resolve , reject jako argumenty (podobně jako exekutor) a čeká, až se jeden z nich zavolá. Ve výše uvedeném příkladu resolve(2) je voláno po 1 sekundě (**) . Poté je výsledek předán dále v řetězci.

Tato funkce nám umožňuje integrovat vlastní objekty s řetězci slibů, aniž bychom museli dědit z Promise .

Větší příklad:načtení

V programování frontend se často používají sliby pro síťové požadavky. Podívejme se tedy na rozšířený příklad.

K načtení informací o uživateli ze vzdáleného serveru použijeme metodu načtení. Má spoustu volitelných parametrů, které jsou popsány v samostatných kapitolách, ale základní syntaxe je docela jednoduchá:

let promise = fetch(url);

Tím se vytvoří síťový požadavek na url a vrátí slib. Příslib se vyřeší pomocí response objekt, když vzdálený server odpoví hlavičkami, ale před stažením úplné odpovědi .

Pro přečtení celé odpovědi bychom měli zavolat metodu response.text() :vrátí příslib, který se vyřeší, když je celý text stažen ze vzdáleného serveru, s tímto textem jako výsledek.

Níže uvedený kód odešle požadavek na user.json a načte svůj text ze serveru:

fetch('/article/promise-chaining/user.json')
 // .then below runs when the remote server responds
 .then(function(response) {
 // response.text() returns a new promise that resolves with the full response text
 // when it loads
 return response.text();
 })
 .then(function(text) {
 // ...and here's the content of the remote file
 alert(text); // {"name": "iliakan", "isAdmin": true}
 });

response objekt vrácený z fetch zahrnuje také metodu response.json() který čte vzdálená data a analyzuje je jako JSON. V našem případě je to ještě pohodlnější, takže na to přejdeme.

Pro stručnost také použijeme funkce šipek:

// same as above, but response.json() parses the remote content as JSON
fetch('/article/promise-chaining/user.json')
 .then(response => response.json())
 .then(user => alert(user.name)); // iliakan, got user name

Nyní udělejme něco s načteným uživatelem.

Můžeme například odeslat další požadavek na GitHub, načíst uživatelský profil a zobrazit avatara:

// Make a request for user.json
fetch('/article/promise-chaining/user.json')
 // Load it as json
 .then(response => response.json())
 // Make a request to GitHub
 .then(user => fetch(`https://api.github.com/users/${user.name}`))
 // Load the response as json
 .then(response => response.json())
 // Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
 .then(githubUser => {
 let img = document.createElement('img');
 img.src = githubUser.avatar_url;
 img.className = "promise-avatar-example";
 document.body.append(img);

 setTimeout(() => img.remove(), 3000); // (*)
 });

Kód funguje; podrobnosti viz komentáře. Je v tom však potenciální problém, typická chyba pro ty, kteří začnou používat sliby.

Podívejte se na řádek (*) :jak můžeme něco udělat poté dokončil se avatar a byl odstraněn? Chtěli bychom například zobrazit formulář pro úpravu tohoto uživatele nebo něčeho jiného. Zatím neexistuje žádný způsob.

Aby byl řetězec rozšiřitelný, musíme vrátit slib, který se vyřeší, když se avatar dokončí.

Takhle:

fetch('/article/promise-chaining/user.json')
 .then(response => response.json())
 .then(user => fetch(`https://api.github.com/users/${user.name}`))
 .then(response => response.json())
 .then(githubUser => new Promise(function(resolve, reject) { // (*)
 let img = document.createElement('img');
 img.src = githubUser.avatar_url;
 img.className = "promise-avatar-example";
 document.body.append(img);

 setTimeout(() => {
 img.remove();
 resolve(githubUser); // (**)
 }, 3000);
 }))
 // triggers after 3 seconds
 .then(githubUser => alert(`Finished showing ${githubUser.name}`));

Tedy .then handler na řádku (*) nyní vrací new Promise , která se vypořádá až po volání resolve(githubUser) v setTimeout (**) . Další .then v řetězci na to počká.

Jako dobrý postup by asynchronní akce měla vždy vrátit slib. To umožňuje plánovat akce po něm; i když nyní neplánujeme prodloužit řetězec, možná jej budeme potřebovat později.

Nakonec můžeme kód rozdělit na opakovaně použitelné funkce:

function loadJson(url) {
 return fetch(url)
 .then(response => response.json());
}

function loadGithubUser(name) {
 return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
 return new Promise(function(resolve, reject) {
 let img = document.createElement('img');
 img.src = githubUser.avatar_url;
 img.className = "promise-avatar-example";
 document.body.append(img);

 setTimeout(() => {
 img.remove();
 resolve(githubUser);
 }, 3000);
 });
}

// Use them:
loadJson('/article/promise-chaining/user.json')
 .then(user => loadGithubUser(user.name))
 .then(showAvatar)
 .then(githubUser => alert(`Finished showing ${githubUser.name}`));
 // ...

Shrnutí

Pokud .then (nebo catch/finally , nezáleží) handler vrátí slib, zbytek řetězce čeká, až se usadí. Když se tak stane, bude jeho výsledek (nebo chyba) předán dále.

Zde je úplný obrázek: