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:
- Počáteční příslib se vyřeší za 1 sekundu
(*)
, - Poté
.then
handler se nazývá(**)
, což zase vytvoří nový příslib (vyřešený pomocí2
hodnotu). - Další
then
(***)
získá výsledek předchozího, zpracuje ho (zdvojnásobí) a předá dalšímu handleru. - …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í:1
→ 2
→ 4
.
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.
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: