Introdução ao Promises

Este guia assume que você tenha certa familiaridade com JavaScript básico e deve ser adequado tanto para pessoas novas em programação assíncrona quanto para aqueles que já tem alguma experiência.

Motivação

Nós queremos que nosso código seja assíncrono, porque se nós escrevemos códigos síncronos então a interface do usuário irá travar (nas aplicações no lado do cliente) ou as requisições não vão ser manipuladas (nas aplicações no lado do servidor). Uma forma de resolver este problema são as threads, mas elas criam seus próprios problemas e elas não são suportadas no JavaScript.

Uma das formas mais simples de se fazer funções assíncronas é aceitando uma função callback. Isso é o que o Node.js faz (no momento em que escrevo). Isto funciona, mas tem um certo número de problemas.

  1. Você perde a separação de entradas e saídas da função, pois o callback deve ser passado como uma entrada.
  2. É difícil de compor múltiplas operações seriais ou paralelas.
  3. Você perde muitas informações úteis da depuração (debugg), e capacidade de manipulação de erros relativos ao rastreamento e execeções.
  4. Você não pode mais usar as construções do fluxo de controle embutidas, e todas elas devem ser reinventadas para trabalhar assíncronamente.

Muitas APIs no navegador usam algum tipo de modelo de evento baseado no fluxo de controle, que resolve o problema 1, mas não os problemas 2 e 4.

Promises tem por objetivo resolver o problema 1 e 3 e pode resolver o problema 4 no ES6 (com o uso de generators).

Uso Básico

A ideia central por trás das promises (promessas) é que uma promise representa um valor que é o resultado de uma operação assíncrona. Elas podem como alternativa vir a ser um erro lançado. Funções assíncronas podem retornar promises:

var prom = get('http://www.example.com');

Se nós requisitarmos o conteúdo da página www.example.com nós vamos estar fazendo isso assíncronamente então podemos receber uma promise de volta.

No intuito de extrair o valor desta promise, nós usamos .done que enfilera uma função a ser executada quando a promise é preenchida com algum resultado.

var prom = get('http://www.example.com');
prom.done(function ( content ) {
    console.log( content );
})

Note como nós passamos uma função que ainda não foi chamada para .done e isso vai ser chamado apenas uma vez, quando o promise estiver preenchido. Nós podemos chamar .done quantas vezes quisermos, antes ou depois, como quisermos, e sempre iremos obter o mesmo resultado. Por exemplo, não há problema em chamá-lo depois que a promise já estiver resolvida:

var cache = {};

function getCache ( url ) {
    if ( cache[url] ) return cache[ url ];
    else return cache[ url ] = get( url );
}

var promA = getCache( 'http://www.example.com' );

promA.done(function ( content ) {
    console.log( content );
});

setTimeout(function () {
    var promB = getCache( 'http://www.example.com' );
    promB.done(function ( content ) {
        console.log( content );
    });
}, 10000);

Claro, requisitar uma página de erro vai facilmente dar errado, e lançar um erro. Por padrão, .done somente lança este erro, então isto fica registrado apropriadamente e (em ambientes diferentes do browser) quebra a aplicação. Nós frequentemente queremos atribuir nosso próprio manipulador:

var prom = get( 'http://www.example.com' );
prom.done(function ( content ) {
    console.log( content );
}, function ( ex ) {
    console.error( 'Requisição www.example.com falhou, você deveria tentar novamente?' );
    console.error( ex.stack );
});

Transformação

Sempre que você tiver uma promise para uma coisa e você precisa fazer algum trabalho nela para obter uma promise para outra coisa. Promises tem um método .then que trabalha um pouco parecido com um .map em um array.

function getJSON ( url ) {
    return get ( url )
        .then(function ( res ) {
            return JSON.parse( res );
        });
}

getJSON( 'htttp://www.example.com/foo.json' ).done(function ( res ) {
    console.log( res );
});

Note como .then manuseia qualquer erro para nós, então eles “sobem” nas pilhas como no código síncrono. VOcê pode também manipulá-los quando você chama .then

Combinação

Uma vantagem de uma promises sendo um valor é que você pode executar operações úteis para combinar promises. Uma dessas operações que a maioria das bibliotecas suportam é:

var a = get('http://www.example.com')
var b = get('http://www.example.co.uk')
var both = Promise.all([a, b])
both.done(function (res) {
  var a = res[0]
  var b = res[1]
  console.dir({
    '.com': a,
    '.co.uk': b
  })
})

Isso é extramamente útil caso você precisa rodar uma série de operações em pararelo. A idéia também se extende a grande arrays, sem limite de valores:


function readFiles(files) {
    return Promise.all(files.map(function (name) {
      return readFile(name)
    }))
}
readFiles(['fileA.txt', 'fileB.txt', 'fileC.txt']).done(function (filesContents) {
  console.dir(filesContents)
})

Naturalmente, operações em série podem ser compostas apenas usando then.


get('http://www.example.com').then(function (res) {
  console.log('.com')
  console.dir(res)
  return get('http://www.example.co.uk')
}).done(function (res) {
  console.log('.co.uk')
  console.dir(res)
})

E com uma pequena imaginação você pode usar esta técnica para ligar com arrays, bem como:


function readFiles(files) {
  var result = []
  
  // create an initial promise that is already fulfilled with null
  var ready = Promise.from(null)
  
  files.forEach(function (name) {
    ready = ready.then(function () {
      return readFile(name)
    }).then(function (content) {
      result.push(content)
    })
  })
  
  return ready.then(function () {
    return result
  })
}
readFiles(['fileA.txt', 'fileB.txt', 'fileC.txt']).done(function (filesContents) {
  console.dir(filesContents)
})