Front End

5 mai, 2014

Escreva códigos JavaScript melhores com Promises

Publicidade

Você provavelmente já ouviu falar por aí sobre como usar promises é o futuro. Todos os caras bacanas estão usando, mas você não vê o que o torna tão especial. Por que não usar apenas um callback? Qual é a grande coisa nisso tudo? Neste artigo, vamos ver do que trata o promises e como você pode usá-lo para escrever melhor seu código JavaScript.

Promises oferece uma leitura fácil

Vamos dizer que você queira pegar alguns dados da API HipsterJesus e adicionar ao seu site. Essa API responde com dados que se parecem com isto:

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

Se você fosse usar um callback, escreveria algo assim:

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

Se você tem experiência com jQuery, vai reconhecer que estamos fazendo uma requisição GET e esperando um JSON na resposta. Também estamos passando uma função callback que pega a resposta JSON e a adiciona ao documento.

Outra forma de escrever isso é usando o objeto promise retornado pelo método getJSON. Você pode adicionar um callback diretamente a esse objeto.

var promise = $.getJSON('http://hipsterjesus.com/api/');

promise.done(function(data) {
  $('body').append(data.text);
});

Assim como no exemplo do callback, isso anexa o resultado da requisição da API ao documento quando a requisição obtém sucesso. Mas o que acontece se a requisição falha? Também podemos anexar um handler fail ao nosso promise.

var promise = $.getJSON('http://hipsterjesus.com/api/');

promise.done(function(data) {
  $('body').append(data.text);
});

promise.fail(function() {
  $('body').append('<p>Oh no, something went wrong!</p>');
});

A maioria das pessoas remove a variável promise, o que torna isso um pouco mais fácil para falar o que o código faz em apenas uma olhada.

$.getJSON('http://hipsterjesus.com/api/')

.done(function(data) {
  $('body').append(data.text);
})

.fail(function() {
  $('body').append('<p>Oh no, something went wrong!</p>');
});

O jQuery também inclui um handler de evento always que é chamado independentemente de a requisição ter sucesso ou falhar.

$.getJSON('http://hipsterjesus.com/api/')

.done(function(data) {
  $('body').append(data.text);
})
.fail(function() {
  $('body').append('<p>Oh no, something went wrong!</p>');
})
.always(function() {
  $('body').append('<p>I promise this will always be added!.</p>');
});

Com promises, a ordem dos callbacks é respeitada. Estamos garantidos de ter nosso callback done primeiro, então o callback fail, e finalmente o callback always.

Melhores APIs

Suponha que você queira crair um objeto wrapper para a API HipsterJesus. Você adiciona o método, html, para retornar os dados HTML que venham da API. Em vez de esse método pegar o handler que é chamado quando a requisição é resolvida, é possível apenas fazer com que o método retorne um objeto promise.

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

O mais legal de tudo isso é que podemos distribuir o objeto promise sem nos preocuparmos com quando ou como ele vai resolver seus valores. Qualquer código que precise retornar valor do promise pode registrar o callback com o done.

O método then permite que se modifique o resultado de um promise e o passe para o próximo handler na cadeia. Isso significa que podemos usar nossa nova API da seguinte forma:

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

Até recentemente, uma das melhores features do AngularJS era que os templates podiam ser vinculados diretamente com o promises. Em um controller Angular, seria assim:

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

Então, seria tão simples como escrever {{ hipsterIpsum.text }} em um template. Quando o promise resolvesse, Angular faria o update da view automaticamente. Infelizmente, o time do Angular desaprovou essa feature. Por enquanto, isso pode ser habilitado ao chamar $parseProvider.unwrapPromises(true). Eu espero que o pessoal do Angular e de outros frameworks incluam essa feature no futuro (viu, Ember?).

Encadeamento

A melhor parte do promises é que você pode fazer um encadeamento! Digamos que você queira adicionar à nossa API um método que retorna um array de parágrafos:

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

Nosso método HTML ficou o mesmo, e o estamos usando no método paragraphs. Como o valor seguinte de um callback do promise é passado para o próximo callback na cadeia, ficamos livres para criar métodos pequenos e funcionais que mudam os dados à medida que é passado por eles.

Podemos encadear promises quantas vezes quisermos. Vamos adicionar um método para as sentenças.

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(/. /);
      }));
    });
  }
};

Chamadas múltiplas

Provavelmente uma das features mais notáveis do promises é a habilidade para combinar múltiplas chamadas de APIs. Quando usamos callbacks, o que acontece se você precisar fazer duas chamadas à API de uma só vez? Provavelmente você vai acabar escrevendo algo assim:

var firstData = null;
var secondData = null;

var responseCallback = function() {

  if (!firstData || !secondData)
    return;

  // do something
}

$.get("http://example.com/first", function(data) {
  firstData = data;
  responseCallback();
});

$.get("http://example.com/second", function(data) {
  secondData = data;
  responseCallback();
});

Com promises, isso se torna muito mais fácil:

var firstPromise = $.get("http://example.com/first");
var secondPromise = $.get("http://example.com/second");

$.when(firstPromise, secondPromise).done(function(firstData, secondData) {
  // do something
});

Aqui estamos usando o método when para anexar um handler que é chamado quando as duas requisições estão feitas.

Conclusão

É isso! Espero que você tenha conseguido vislumbrar as coisas maravilhosas que pode fazer com promises. Qual a sua forma favorita de usar? Conte nos comentários!

*Nota: Para tornar mais simples, este artigo está usando uma implementação jQuery deferred. Há algumas diferenças entre o objeto jQuery Deferred e o Promises/A+ epecificação, que é um padrão mais canônico. Para mais informações, veja a a nossa wiki Coming from jQuery.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://davidwalsh.name/write-javascript-promises