Promise je obecně definován jako objekt, který obsahuje funkci jako hodnotu atributu then. Tato metoda then přijímá jako parametr succesCallback, dále volitelně errorCallback, a vrací nový promise. Promise se může nacházet ve třech různých stavech: nenaplněn (unfulfilled), naplněn (fulfilled) a odmítnut (rejected), v každém okamžiku se nachází pouze v jednom stavu a jakmile byl naplněn nebo odmítnut, jeho stav se již nezmění.
Řešení asynchroních operací pomocí tohoto vzoru spočívá v tom, že asynchronní funkce vrací promis (slib, jinak také 'budoucí hodnotu'). Na tomto promisu se pomocí funkce then zaregistruje succesCallback, který bude zavolán, jakmile dojde k naplnění tohoto promisu (případně lze ještě zaregistrovat errorCallback pro řešení problému v případě zamítnutí).
Nyní ke konkrétní implementaci návrhového vzoru promise v AngularJS. Použití je demonstrováno na následujícím jednoduchém příkladě:
function asyncFunction() { var deferred = $q.defer(); setTimeout(function() { if(Math.round(Math.random())) { deferred.resolve('resolved'); } else { deferred.reject('rejected'); } }, 2000); return deferred.promise;
Vzor promise je implementován v modulu $q. V asynchronní funkci se vytvoří zpožděná proměnná deferred voláním $q.defer(). Přistoupit k promise lze pomocí deferred.promise. Po vytvoření deferred je příslušný promise ve stavu unfulfilled, do stavu fulfilled se přepne pomocí deferred.resolve(), do stavu rejected pomocí deferred.reject(). Prostřednictvým těchto dvou funkcí lze příslušným callbackům předat jediný parametr, tzn. po deferred.resolve(object) je volán succesCallback(object). Na konci asynchroní funkce je potřeba tento promis vrátit.
Následuje registrace callback funkcí k vrácenému promise. V momentě použití této asynchroní funkce si na vrácený promis můžeme zaregistrovat callback funkce následujícím způsobem :
var async_promise = async_function(); async_promise.then(succesCallback, errorCallback);
Vzhledem k tomu, že metoda then vrací promise, lze jednotlivé promises řetězit
async_promise.then(succesCallback1).then(succesCallback2).then(succesCallback3)
then vrací promis, který je návratovou hodnotou succesCallback. V případě, že např. succesCallback2 vrací přes return rovnou hodnotu (ne promise), pak je následující funkce succesCallback3 volána ihned, avšak stále asynchroně (volá se pomocí setTimout(0)). Na pozadí se tím pádem pravděpodobně sám vytvoří promise.
Pokud bychom chtěl provést nějakou operaci nehledě na to, jestli byl promise naplněn, nebo zamítnut, lze tuto operaci na promis zaregistrovat následovně :
promise.finally(finalCallback)
V případě, že máme několik asynchroních operací a chceme zavolat funkci po dokončení všech těchto funkcí, slouží k tomu metoda $q.all(arrayOfPromises). Máme např. 3 async funkce vracející promise1, promise2 a promise3, pak zaregistrujeme funkci function allFinished() { … } následovně :
$q.all([promise1, promise2, promise3]).then(allFinished())
Funkce $q.all() navrací promise, který je naplněn ve chvíli, kdy je naplněn každý promise v předaném poli.
Pro vytváření promise v Angular 2 používáme generickou třídu Promise<T>, které jako parametr vložíme lambda výraz, s parametry resolve, reject, který se provede. Generická hodnota T je potom typ, který promisa vrací
function asyncFunction(): Promise<string> { return new Promise<string>((resolve, reject) => { this.http.get("getTokenAsString").map((res: Response) => res.json()).toPromise().then((res: any) => { if (res.status == "ok") { resolve(res.token); } else { reject("No token received"); } }); }); }
Při odchytnutí více promis najednou je pak možné použít následující funkci:
Promise.all([array promisů]).then(() => {/*Všechno splněno*/}).catch(() => {/*Chyba*/});
Při používání několika promis za sebou však můžeme dojít k nepříjemné věci a to té, že máme veliké množství úrovní v kódu a ten se stává nepřehledným, viz příklad:
private initTest(): Promise<any> { return new Promise((reject: any, resolve: any): void => { this.storage.get("token").then((token: string) => { this.storage.get("user").then((user: string) => { this.http.get("isLoggedIn/" + user + token).map((res: Response) => res.json()).toPromise().then((res: any) => { if (res.status == "ok") { this.storage.set("token", res.token).then((res: any) => { this.storage.set("user", JSON.stringify(res.user)).then((res: any) => { resolve(res.user); }); }); } }); }); }); }); }
Tento kód se dá však pomocí klíčových slov await a async výrazně zjednodušit, jde o to, že funkci přidáme do hlavičky klíčové slovo async, čímž řekneme, že funkce funguje asynchronně a tím pádem vrací promise. Následně pak můžeme používat při volání funkcí vracejících promisy klíčové slovo await, které místo promisy vrátí do proměnné přímo její návratovou hodnotu. A pokud bude promisa rejectnutá, vyhodí klasický try/catch error.
Stejná funkce jako je výše by se s tímto způsobem dala napsat následovně:
private async initTest(): Promise<any> { try { let token: string = await this.storage.get("token"); let user: string = await this.storage.get("user"); let res: any = this.http.get("isLoggedIn/" + user + token).map((res: Response) => res.json()).toPromise(); if (res.status == "ok") { let savedToken: any = await this.storage.set("token", res.token); let savedUser: any = await this.storage.set("user", JSON.stringify(res.user)); return user; } return undefined; } catch (ex) { return undefined; } }
Tahle funkce vezme kód který je napsaný asynchronně, provede awaity za sebou, vždy čeká, až se jeden dokončí, aby mohla spustit druhý a poté hodnotu kterou vrátíte vrátí jako resolve. Reject se dá vrátit jednoduchým vyhozením výjimky
throw new Error("error")
A nezapomnělo se ani na provádění více promis zároveň, k tomu se znovu hodí výše popsaná funkce Promise.all, zápis pak vypadá následovně:
const [res1, res2] = await Promise.all([func1(), func2()]);
Více informací o asynchronních funkcích v typescriptu najdete na téhle stránce: http://2ality.com/2016/10/async-function-tips.html