Slib

Představte si, že jste špičkový zpěvák a fanoušci dnem i nocí žádají vaši nadcházející skladbu.

Abyste si trochu ulevili, slíbíte, že jim to pošlete, až bude zveřejněn. Dáte svým fanouškům seznam. Mohou vyplnit své e-mailové adresy, takže jakmile bude skladba dostupná, všechny přihlášené strany ji okamžitě obdrží. A i když se něco velmi pokazí, řekněme, dojde k požáru ve studiu, takže skladbu nebudete moci publikovat, budou o tom informováni.

Všichni jsou šťastní:vy, protože vás lidé už netlačí, a fanoušci, protože jim píseň neuteče.

Toto je reálná analogie věcí, které často máme v programování:

  1. Produkce kódu, která něco dělá a vyžaduje čas. Například nějaký kód, který načte data přes síť. To je „zpěvák“.
  2. „Spotřebitelský kód“, který požaduje výsledek „vytvoření kódu“, jakmile bude připraven. Mnoho funkcí může vyžadovat tento výsledek. To jsou „fanoušci“.
  3. slib je speciální objekt JavaScriptu, který spojuje „produkující kód“ a „spotřebovávající kód“. Z hlediska naší analogie:toto je „seznam předplatitelů“. „Vytvoření kódu“ zabere tolik času, kolik potřebuje k vytvoření slíbeného výsledku, a „příslib“ zpřístupní tento výsledek všem přihlášeným kódům, když je připraven.

Tato analogie není příliš přesná, protože přísliby JavaScriptu jsou složitější než jednoduchý seznam předplatných:mají další funkce a omezení. Ale pro začátek je dobré.

Syntaxe konstruktoru pro objekt slibu je:

let promise = new Promise(function(resolve, reject) {
 // executor (the producing code, "singer")
});

Funkce předána do new Promise se nazývá exekutor . Když new Promise je vytvořen, exekutor se spustí automaticky. Obsahuje produkční kód, který by měl nakonec produkovat výsledek. Ve smyslu výše uvedené analogie:vykonavatelem je „zpěvák“.

Jeho argumenty resolve a reject jsou zpětná volání poskytovaná samotným JavaScriptem. Náš kód je pouze uvnitř exekutoru.

Když exekutor obdrží výsledek, ať už brzy nebo pozdě, na tom nezáleží, měl by zavolat jedno z těchto zpětných volání:

  • resolve(value) — pokud je úloha úspěšně dokončena, s výsledkem value .
  • reject(error) — pokud došlo k chybě, error je objekt chyby.

Abychom to shrnuli:exekutor běží automaticky a pokouší se provést úlohu. Po dokončení pokusu zavolá resolve pokud to bylo úspěšné nebo reject pokud došlo k chybě.

promise objekt vrácený new Promise konstruktor má tyto vnitřní vlastnosti:

  • state — zpočátku "pending" , pak se změní buď na "fulfilled" když resolve se nazývá nebo "rejected" když reject se nazývá.
  • result — původně undefined , pak se změní na value když resolve(value) se nazývá nebo error když reject(error) se nazývá.

Takže exekutor nakonec přesune promise do jednoho z těchto stavů:

Později uvidíme, jak se „fanoušci“ mohou přihlásit k odběru těchto změn.

Zde je příklad konstruktoru slibů a jednoduché funkce spouštěče s „vytvářením kódu“, které zabere čas (přes setTimeout ):

let promise = new Promise(function(resolve, reject) {
 // the function is executed automatically when the promise is constructed

 // after 1 second signal that the job is done with the result "done"
 setTimeout(() => resolve("done"), 1000);
});

Spuštěním výše uvedeného kódu můžeme vidět dvě věci:

  1. Exekutor je volán automaticky a okamžitě (pomocí new Promise ).

  2. Exekutor obdrží dva argumenty:resolve a reject . Tyto funkce jsou předdefinované enginem JavaScript, takže je nemusíme vytvářet. Měli bychom zavolat pouze jednomu z nich, až budeme připraveni.

    Po jedné sekundě „zpracování“ zavolá exekutor resolve("done") produkovat výsledek. Tím se změní stav promise objekt:

To byl příklad úspěšného dokončení práce, „splněného slibu“.

A nyní příklad, kdy exekutor odmítl slib s chybou:

let promise = new Promise(function(resolve, reject) {
 // after 1 second signal that the job is finished with an error
 setTimeout(() => reject(new Error("Whoops!")), 1000);
});

Volání na reject(...) přesune objekt slibu na "rejected" stav:

Abych to shrnul, exekutor by měl provést úlohu (obvykle něco, co vyžaduje čas) a poté zavolat resolve nebo reject změnit stav odpovídajícího objektu slibu.

Slib, který je vyřešen nebo odmítnut, se nazývá „vypořádaný“, na rozdíl od původně „nevyřízeného“ slibu.

Může existovat pouze jeden výsledek nebo chyba

Exekutor by měl volat pouze jeden resolve nebo jeden reject . Jakákoli změna stavu je konečná.

Všechna další volání resolve a reject jsou ignorovány:

let promise = new Promise(function(resolve, reject) {
 resolve("done");

 reject(new Error("…")); // ignored
 setTimeout(() => resolve("…")); // ignored
});

Myšlenka je taková, že práce provedená exekutorem může mít pouze jeden výsledek nebo chybu.

Také resolve /reject očekávat pouze jeden argument (nebo žádný) a další argumenty bude ignorovat.

Odmítnout pomocí Error objektů

V případě, že se něco pokazí, měl by exekutor zavolat reject . To lze provést s libovolným typem argumentu (stejně jako resolve ). Doporučuje se však použít Error objekty (nebo objekty, které dědí z Error ). Důvody pro to budou brzy zřejmé.

Okamžitě volejte na číslo resolve /reject

V praxi exekutor obvykle dělá něco asynchronně a volá resolve /reject po nějaké době, ale nemusí. Můžeme také zavolat resolve nebo reject okamžitě, takto:

let promise = new Promise(function(resolve, reject) {
 // not taking our time to do the job
 resolve(123); // immediately give the result: 123
});

K tomu může například dojít, když začneme dělat nějakou práci, ale pak zjistíme, že vše již bylo dokončeno a uloženo do mezipaměti.

To je v pořádku. Okamžitě máme vyřešený slib.

state a result jsou interní

Vlastnosti state a result objektu Promise jsou interní. Nemáme k nim přímý přístup. Můžeme použít metody .then /.catch /.finally pro to. Jsou popsány níže.

Spotřebitelé:pak chyťte

Objekt Promise slouží jako spojovací článek mezi exekutorem ("produkující kód" nebo "zpěvák") a konzumujícími funkcemi ("fanoušci"), které obdrží výsledek nebo chybu. Spotřební funkce lze registrovat (předplatit) pomocí metod .then a .catch .

pak

Nejdůležitější, základní je .then .

Syntaxe je:

promise.then(
 function(result) { /* handle a successful result */ },
 function(error) { /* handle an error */ }
);

První argument z .then je funkce, která se spustí, když je příslib vyřešen a obdrží výsledek.

Druhý argument .then je funkce, která se spustí, když je příslib odmítnut a obdrží chybu.

Zde je například reakce na úspěšně vyřešený slib:

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

// resolve runs the first function in .then
promise.then(
 result => alert(result), // shows "done!" after 1 second
 error => alert(error) // doesn't run
);

Byla provedena první funkce.

A v případě odmítnutí druhý:

let promise = new Promise(function(resolve, reject) {
 setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject runs the second function in .then
promise.then(
 result => alert(result), // doesn't run
 error => alert(error) // shows "Error: Whoops!" after 1 second
);

Pokud nás zajímají pouze úspěšná dokončení, můžeme pro .then poskytnout pouze jeden argument funkce :

let promise = new Promise(resolve => {
 setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // shows "done!" after 1 second

chytit

Pokud nás zajímají pouze chyby, můžeme použít null jako první argument:.then(null, errorHandlingFunction) . Nebo můžeme použít .catch(errorHandlingFunction) , což je úplně stejné:

let promise = new Promise((resolve, reject) => {
 setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second

Volání .catch(f) je úplný analog .then(null, f) , je to jen zkratka.

Úklid:konečně

Stejně jako existuje finally klauzule v běžném try {...} catch {...} , je zde finally ve slibech.

Volání .finally(f) je podobný .then(f, f) v tom smyslu, že f běží vždy, když je příslib splněn:ať už je vyřešen nebo odmítnut.

Myšlenka finally je nastavit obslužnou rutinu pro provádění čištění/finalizace po dokončení předchozích operací.

Např. zastavení indikátorů načítání, uzavření již nepotřebných spojení atd.

Představte si to jako zakončení večírku. Bez ohledu na to, zda byla párty dobrá nebo špatná, kolik přátel na ní bylo, stále po ní potřebujeme (nebo bychom alespoň měli) provést úklid.

Kód může vypadat takto:

new Promise((resolve, reject) => {
 /* do something that takes time, and then call resolve or maybe reject */
})
 // runs when the promise is settled, doesn't matter successfully or not
 .finally(() => stop loading indicator)
 // so the loading indicator is always stopped before we go on
 .then(result => show result, err => show error)

Vezměte prosím na vědomí, že finally(f) není přesně alias then(f,f) ačkoli.

Jsou zde důležité rozdíly:

  1. A finally handler nemá žádné argumenty. V finally nevíme, zda je slib úspěšný nebo ne. To je v pořádku, protože naším úkolem je obvykle provádět „obecné“ dokončovací postupy.

    Podívejte se prosím na výše uvedený příklad:jak vidíte, finally handler nemá žádné argumenty a výsledek slibu zpracuje další handler.

  2. A finally handler „předá“ výsledek nebo chybu dalšímu vhodnému handleru.

    Například zde je výsledek předán přes finally na then :

    new Promise((resolve, reject) => {
     setTimeout(() => resolve("value"), 2000);
    })
     .finally(() => alert("Promise ready")) // triggers first
     .then(result => alert(result)); // <-- .then shows "value"

    Jak můžete vidět, value vrácený prvním příslibem je předán přes finally na další then .

    To je velmi výhodné, protože finally není určen ke zpracování slibovaného výsledku. Jak již bylo řečeno, je to místo, kde se provádí obecné čištění, bez ohledu na to, jaký byl výsledek.

    A zde je příklad chyby, abychom viděli, jak byla předána přes finally na catch :

    new Promise((resolve, reject) => {
     throw new Error("error");
    })
     .finally(() => alert("Promise ready")) // triggers first
     .catch(err => alert(err)); // <-- .catch shows the error
  3. A finally psovod by také neměl nic vracet. Pokud ano, vrácená hodnota je tiše ignorována.

    Jedinou výjimkou z tohoto pravidla je finally handler vyvolá chybu. Potom tato chyba přejde na další obslužnou rutinu namísto jakéhokoli předchozího výsledku.

Abych to shrnul:

  • A finally handler nezíská výsledek předchozího handleru (nemá žádné argumenty). Tento výsledek je místo toho předán dalšímu vhodnému handleru.
  • Pokud finally handler něco vrátí, je to ignorováno.
  • Když finally vyvolá chybu, pak provedení přejde na nejbližší obslužnou rutinu chyb.

Tyto funkce jsou užitečné a umožňují, aby věci fungovaly správně, pokud použijeme finally jak se má používat:pro obecné postupy čištění.

Můžeme připojit handlery k dohodnutým slibům

Pokud příslib čeká na vyřízení, .then/catch/finally handleři čekají na jeho výsledek.

Někdy se může stát, že slib je již splněn, když k němu přidáme handler.

V takovém případě se tyto obslužné programy okamžitě spustí:

// the promise becomes resolved immediately upon creation
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done! (shows up right now)

Všimněte si, že díky tomu jsou sliby silnější než scénář „seznamu předplatitelů“ v reálném životě. Pokud zpěvák již vydal svou píseň a poté se někdo zapíše do seznamu předplatitelů, pravděpodobně tuto píseň neobdrží. Předplatné v reálném životě musí být provedeno před událostí.

Sliby jsou flexibilnější. Obslužné rutiny můžeme přidat kdykoli:pokud již existuje výsledek, pouze se provedou.

Příklad:loadScript

Dále se podívejme na další praktické příklady toho, jak nám sliby mohou pomoci napsat asynchronní kód.

Máme loadScript funkce pro načtení skriptu z předchozí kapitoly.

Zde je varianta založená na zpětném volání, abychom si ji připomněli:

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);
}

Přepišme to pomocí Promises.

Nová funkce loadScript nebude vyžadovat zpětné volání. Místo toho vytvoří a vrátí objekt Promise, který se vyřeší po dokončení načítání. Vnější kód k němu může přidat handlery (předplatné funkce) pomocí .then :

function loadScript(src) {
 return new Promise(function(resolve, reject) {
 let script = document.createElement('script');
 script.src = src;

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

 document.head.append(script);
 });
}

Použití:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
 script => alert(`${script.src} is loaded!`),
 error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

Okamžitě vidíme několik výhod oproti vzoru založenému na zpětném volání:

Sliby Zpětná volání
Sliby nám umožňují dělat věci v přirozeném řádu. Nejprve spustíme loadScript(script) a .then napíšeme, co dělat s výsledkem. Musíme mít callback funkce, kterou máme k dispozici při volání loadScript(script, callback) . Jinými slovy, musíme vědět, co s výsledkem dělat před loadScript se nazývá.
Můžeme zavolat .then na slib tolikrát, kolikrát chceme. Pokaždé přidáváme do „seznamu předplatitelů“ nový „fanoušek“, novou funkci přihlášení k odběru. Více o tom v další kapitole:Řetězení slibů. Může být pouze jedno zpětné volání.

Sliby nám tedy poskytují lepší tok kódu a flexibilitu. Ale je toho víc. To uvidíme v dalších kapitolách.