Jak na lepší JavaScript s promisy?

24. března 2015

Možná jste již zaslechli názory na to, že budoucnost JavaScriptu je v promisech. Všichni skvělí dynamičtí borci už jsou v nich jako doma. Objevili jste i vy v čem má spočívat jejich výjimečnost? Nestačí prostě použít nějaký callback? Jsou opravdu tak mimořádně důležité? V tomto článku se dozvíte, co jsou promisy zač a jak s nimi psát lepší JavaScript.

Co jsou to vlastně promisy?

Promise a Deferred jsou již dlouho známé programovací konstrukce, kde promise („příslib“) vyjadřuje hodnotu, která ještě není známá, a deferred („pozdržení“) vyjadřuje práci, která ještě není dokončená.

Každá nedokončená práce (deferred) má svůj „příslib“ hodnoty (promise), ten zase má zpracovatele (handlers), kteří říkají, co se má udělat, až se ta práce dokončí a (nebo) bude známa hodnota přislibu, a dále má tři stavy (states) atd.

Promisy jsou srozumitelnější

Řekněme, že chcete shrábnout nějaká data z HipsterJesus API a přidat je do své stránky. Toto API odpovídá daty, která vypadají následovně:

{
"text": "<p>Lorem ipsum...</p>",
"params": {
"paras": 4,
"type": "hipster-latin"
}
}

Pomocí volání zpět bychom napsali něco podobného tomuto:

$.getJSON('http://hipsterjesus.com/api/', function(data) {
$('body').append(data.text);
});

Pokud máte zkušenosti s jQuery, rozpoznali jste, že činíme GET požadavek a očekáváme JSON (JavaScript Object Notation, datový formát nezávislý na platformě určený pro přenos dat) v těle odpovědi. Předáváme také funkci volání zpět, která převezme odpověď JSON a přidá ji do dokumentu.

Dá se to ale také napsat jinak, když se využije objekt promise, který vrací metoda getJSON. Volání zpět můžete připojit přímo k tomuto objektu.

var promise = $.getJSON('http://hipsterjesus.com/api/');
promise.done(function(data) {
$('body').append(data.text);
});

Podobně jako v ukázce volání zpět, i zde se, pokud bude požadavek úspěšný, přidá výsledek API požadavku do dokumentu. Co se ale stane, pokud se požadavek nezdaří? K našemu „příslibu“, promise, můžeme také připojit zpracovatele nezdaru, fail.

var promise = $.getJSON('http://hipsterjesus.com/api/');
 
promise.done(function(data) {
$('body').append(data.text);
});
 
promise.fail(function() {
$('body').append('<p>Ale ne, něco se nepodařilo!</p>');
});

Většina lidí dává proměnnou promise pryč, protože je pak zřejmé na první pohled, co kód dělá.

$.getJSON('http://hipsterjesus.com/api/')
.done(function(data) {
$('body').append(data.text);
})
.fail(function() {
$('body').append('<p>Ale ne, něco se nepodařilo!</p>');
});

jQuery také obsahuje událostního zpracovatele always, který se volá bez ohledu na to, zda požadavek uspěl, nebo ne.

$.getJSON('http://hipsterjesus.com/api/')
 
.done(function(data) {
$('body').append(data.text);
})
.fail(function() {
$('body').append('<p>Ale ne, něco se nepodařilo!</p>');
})
.always(function() {
$('body').append('<p>Slibuji, že se tohle přidá vždycky!</p>');
});

Když pracujeme s promisy, bude se respektovat pořadí volání zpět. Máme zaručeno, že se nejprve zavolá volání zpět done, pak volání zpět fail, a nakonec volání zpět always.

Lepší API

Řekněme, že chceme pro HipsterJesus API vytvořit nějaký obalový objekt. Přidáme proto nějakou metodu, html, aby vrátila HTML data, která přišla z tohoto API. Místo toho, abychom museli tuto metodu dávat do nějakého zpracovatele, který se zavolá poté, co byl požadavek vyřešen, necháme prostě metodu vrátit objekt promise.

var hipsterJesus = {
html: function() {
return $.getJSON('http://hipsterjesus.com/api/').then(function(data) {
return data.text;
});
}
};

Skvělé je, že můžeme objekt promise předávat dál, aniž bychom se starali o to, kdy nebo jak vyřeší svou hodnotu. Jakýkoli kód, který potřebuje návratovou hodnotu promise, může prostě zaregistrovat nějaké volání zpět s done.

Metoda then umožňuje modifikovat výsledek promise, a předat ho následujícímu zpracovateli v řetězu zpracovatelů. To znamená, že naše nové API můžete používat v tomto stylu:

hipsterJesus.html().done(function(html) {
$("body").append(html);
});

Až donedávna bylo jednou z omračujících schopností AngularJS to, že se šablony mohly přímo vázat na promisy. V řadiči angularu to vypadalo takhle:

$scope.hipsterIpsum = $http.get(‚http://hipsterjesus.com/api/‘);

Pak v nějaké šabloně stačilo napsat {{ hipsterIpsum.text }}, víc nebylo třeba. Když se promise vyřešil, Angular zobrazení automaticky aktualizoval. Bohužel, tým Angularu prohlásil tuto schopnost za překonanou. Prozatím se dá zapnout tím, že se zavolá $parseProvider.unwrapPromises(true). Doufám, že Angular i další frameworky v dohledné budoucnosti zase tuto schopnost zařadí (Sleduju tě, Embere!).

Řetězení

Nejlepší částí příběhu o promisech je to, že je můžete řetězit! Řekněme, že do svého API chcete přidat metodu, která vrací pole odstavců.

var hipsterJesus = {
 
html: function() {
return $.getJSON('http://hipsterjesus.com/api/').then(function(data) {
return data.text;
});
},
 
paragraphs: function() {
return this.html().then(function(html) {
return html.replace(/<[^>]+>/g, "").split("");
});
}
};

Naši HTML metodu jsme ponechali stejnou a použili jsme ji v metodě paragraphs. Protože se návratová hodnota volání zpět promisu předává do dalšího volání zpět daného řetězu, můžeme klidně vytvářet malé, funkční metody, které mění data, když se skrz ně předávají.

Promisy můžeme řetězit tolikrát, kolikrát potřebujeme. Přidejme metodu pro věty.

var hipsterJesus = {
 
html: function() {
return $.getJSON('http://hipsterjesus.com/api/').then(function(data) {
return data.text;
});
},
 
paragraphs: function() {
return this.html().then(function(html) {
return html.replace(/<[^>]+>/g, "").split("");
});
},
 
sentences: function() {
return this.paragraphs().then(function(paragraphs) {
return [].concat.apply([], paragraphs.map(function(paragraph) {
return paragraph.split(/. /);
}));
});
}
};

Hromadná volání

Asi nejpozoruhodnější schopností promisů je jejich způsobilost kombinovat více API volání. Když používáte volání zpět, jak to uděláte, pokud potřebujete učinit najednou dvě API volání? Pravděpodobně skončíte s něčím takovýmhle:

var firstData = null;
var secondData = null;
 
var responseCallback = function() {
 
if (!firstData || !secondData)
return;
 
// zde se něco dělá
}
 
$.get("http://example.com/first", function(data) {
firstData = data;
responseCallback();
});
 
$.get("http://example.com/second", function(data) {
secondData = data;
responseCallback();
});

S promisy je to mnohem jednodušší:

var firstPromise = $.get("http://example.com/first");
var secondPromise = $.get("http://example.com/second");
 
$.when(firstPromise, secondPromise).done(function(firstData, secondData) {
// zde se něco dělá
});

Pomocí metody when připojujeme zpracovatele, který se zavolá, až budou hotové oba požadavky.

Závěr

A je to! Doufám, že už teď máte jistou představu, jak báječné věci se dají docílit s promisy. Jaký je váš nejoblíbenější způsob jejich využití? Dejte vědět v komentářích!

*Poznámka: kvůli jednoduchosti používá tento článek implementaci deferred jQuery. Existují jisté jemné rozdíly mezi objektem Deferred a specifikací Promises/A+, což je oficiálnější standard. Další informace si ověřte v článku Coming from jQuery nástroje Q.

Kdo je Landon Schropp

Landon je vývojář, designér a podnikatel sídlící v Seattle. S nadšením buduje jednoduché aplikace, které pak lidé prostě milují.

Původní článek: Write better JavaScript with promises

Zdrojové kódy jsou pod licencí MIT.

  • Translation: RNDr. Jan Pokorný
  • Language and expert collaboration: Marek Machač
Štítky: javascript

Mohlo by vás také zajímat

Nejnovější

4 komentářů

  1. Aleš

    Bře 25, 2015 v 16:23

    Volání zpět? Myslíte, že je vhodné používat překlad zrovna tohohle?

    Odpovědět
    • Marek Machač

      Bře 26, 2015 v 9:54

      Děkuji za upozornění. Nechal jsem pouze anglický výraz.

      Odpovědět
      • miro-jk

        Úno 20, 2019 v 20:04

        nj, jenže ono je to v tom textu ještě mnohokrát, fakt se to čte blbě :)

        Odpovědět
  2. Michal Kleiner

    Bře 25, 2015 v 21:52

    „volání zpět“ je fakt hrozný obrat

    Odpovědět

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *