Promise v Javascriptu

Michael JavaScript

Sliby chyby? Co je Promise a jakým způsobem mění asynchronní programování v Javascriptu?

Promise v Javascriptu

Asynchronnímu programování v Javascriptu se dnes už vyhnete jen málokdy. Často je potřeba reagovat na události uživatelského rozhraní, stahovat nebo posílat data serveru, načítat soubory, apod. Nejlépe všechno najednou. Přitom každá taková akce může trvat libovolně dlouhou dobu.

Callback

Nejjednodušším řešením asynchronních akcí je použití callbacků. Metodě předáme callback funkci, která bude spuštěna, jakmile se metoda dokončí. Ta mezitím, co probíhá, neblokuje vlákno a aplikace pokračuje v běhu. V příkladu níže napodobuje funkce reverseAsync čekání na odpověď ze serveru. Přijímá text a callback funkci jako parametry a po 1 sekundě vrátí spustí callback s převráceným textem.

// Callback je zavolán s výsledkem, jakmile se metoda dokončí
function reverseAsync(message, callback) {

  setTimeout(() => {
    const reversedMessage = message.split('').reverse().join('')
    callback(reversedMessage)
  }, 1000)

}

// Volání s callbackem
reverseAsync('Ahoj', (reversed) => console.log(reversed)) // 'johA'

Sliby

Promise na to jde jinak. Metodě nepředáváme callback. Místo toho metoda vrátí Promise objekt. Ten funguje jako "příslib" budoucí hodnoty, kterou metoda jednou vrátí. Lze s ním dále pracovat, předávat ho a tím je nezávislý na čase. Je to podobné, jako když si objednáte bagetu, dostanete papírek s pořadovým číslem a čekáte. Bagetu ještě nemáte fyzicky v ruce, ale přesto s ní můžete počítat. Máte v ruce něco, díky čemu můžete plánovat, kde a jak si bagetu vychutnáte. Stejně tak si klidně odskočíte a později se vrátíte, aby jste si bagetu vyzvedli. Pracujete s referencí budoucí hodnoty.

Výsledek Promise si zase vyzvednete pomocí metody then(). Je jedno, jestli už byl vyřešen v čase dříve, než došlo k zavolání then(). Bude k dispozici i tak. První příklad by s Promise vypadal takto:

// Místo předání callbacku, vrátí funkce Promise objekt
// se kterým lze dále pracovat
function reverseAsync(message) { 

  return new Promise((resolve, reject) => {
    setTimeout(() => { 
      const reversedMessage = message.split('').reverse().join('') 
      resolve(reversedMessage) 
    }, 1000)
  })

} 

// S Promise objektem lze dále pracovat
const promise = reverseAsync('Ahoj')

promise.then(reversed => console.log(reversed)) // 'johA'

V porovnání obou příkladů není řešení až tak odlišné. Nakonec u obou musíme předat nějaký callback a první řešení je navíc o něco kratší a přehlednější.

Callback hell

Skutečná výhoda Promise ale není v jediném volání. Je v koordinaci řetězce více asynchronních volání. Ta mohou být na sobě různě závislá a v různém pořadí. Typická je situace, kdy musíme na základě výsledku jednoho volání serveru provést druhé. Například první volání vrátí výpis uživatelů a pro každého uživatele pak chceme druhým voláním získat výpis jeho příspěvků.

Řekněme, že k funkci z prvního příkladu máme ještě funkci, která po nějaké době vrátí text s pouze prvním písmenem velkým. Tu chci zavolat až, jakmile budu mít výsledek první funkce:

reverseAsync('Ahoj', (reversed) => {
  uppercaseFirstAsync(reversed, (uppercased) => {
    console.log(uppercased) // 'Joha'
  })
})

S každým dalším závislým callbackem, který bychom přidali, se noříme hlouběji a hlouběji. Můžeme tak velmi rychle dosáhnout takové pyramidy callbacků, že i tisíce let po nás se budou divit, jak jsme to dokázali. Tento jev je obecně známý jako callback hell.

Zkusme příklad vyřešit pomocí Promise:

reverseAsync('Ahoj')
  .then(reversed => uppercaseFirstAsync(reversed))
  .then(uppercased => console.log(uppercased)) // 'Joha'

Obě funkce reverseAsync i uppercaseFirstAsync vracejí objekt Promise. Výsledkem prvního then() tedy bude další Promise, který může být opět vyřešený stejným způsobem.

Chyby

Kromě toho, že je výsledná struktura mnohem přehlednější, snadněji se také ošetřují chyby. U callbacků bychom museli chyby ošetřit na každé úrovni zanoření. V případě Promise zachytí metoda catch() každou chybu, která z řetězce doputuje.

reverseAsync('Ahoj')
  .then(reversed => uppercaseFirstAsync(reversed))
  .then(uppercased => console.log(uppercased))
  .catch(error => console.log(`Vyskytla se chyba: ${error}`))

Vyhození chyby je při tvorbě nového Promise objektu možné zavoláním druhého parametru vstupní funkce. Pokud bychom chtěli ve funkci reverseAsync při překročení určité délky vstupu vyvolat chybu:

function reverseAsync(message) { 
  return new Promise((resolve, reject) => { 
    setTimeout(() => { 

      const reversedMessage = message.split('').reverse().join('')

      // Pokud je délka vstupu větší než 10 znaků,
      // Promise objekt bude vyřešen jako chybový
      if (message.length < 10) resolve(reversedMessage)
      else reject(`Text "${message}" je delší, než 10 znaků!`)

    }, 1000) 
  })
}

reverseAsync('Kotě v botě')
  .then(reversed => console.log(reversed))
  .catch(error => console.log(error)) 
  // 'Text "Kotě v botě" je delší, než 10 znaků!'

Promise.all()

Díky tomu lze k asynchronnímu programování přistupovat funkcionálně. Např. pro pole textů, které chceme asynchronně přetvořit, nemusíme řešit callback pro každý text zvlášť. Promise API vystavuje metodu Promise.all, která vezme pole Promise objektů a vrátí jediný. Ten se vyřeší, jakmile budou všechny v poli dokončeny:

// Přemění pole stringů na pole Promise objektů
let promises = ['Kočka', 'Leze', 'Dírou']
  .map(word => reverseAsync(word))

console.log(promises) 
// '[object Promise, object Promise, ...]'

// Vrátí jediný Promise pro všechny v poli
Promise.all(promises)
  .then(reversedWords => console.log(reversedWords)) 
  // '['akčoK', 'ezeL', 'uoríD']'

Výsledkem je pole přetvořené async. funkcí. To také zachovává původní řazení za předpokladu, že i vstup je seřazený (např. když je na vstupu právě pole).

Závěr

Přes všechny klady není Promise žádným zázračným všelékem na neduhy asynchronního programování. Potýká se se svými vlastními problémy, mezi které patří třeba zpracování streamu asynchronních událostí. Jednou jeho vlastností je totiž, že může být vyřešen pouze jednou. Tato omezení vedou k vytváření nových přístupů, z těch stojí za zmínku především Observable z knihovny RxJS od Microsoftu.

Promise se ze strany moderních prohlížečů těší dobré podpoře. Jestli však potřebujete zajistit podporu i u těch starších, doporučuji Babel a jeho polyfill.


Chcete se podělit o zkušenosti s Promise? Napište nám do komentářů.

Přidat komentář

Právě odpovídáte na existující komentář. Zrušit

Komentáře

Novinky z blogu

Přidání balíčku do Composeru bez Packagist

Composer umožňuje přidat balíček, který není zveřejněn na Packagist. Stačí, aby byl ve veřejném či privátním git repozitáři, dostupný lokálně na serveru v jiné složce nebo...

Další články