Até pouco tempo atrás, o sistema de promessas no JavaScript era um assunto fora da minha área de conhecimento. Com a oportunidade de trabalhar em um projeto isomórfico com JavaScript, ou seja, a aplicação rodando no servidor (NodeJs) e cliente (browser), as coisas começaram a ficar mais claras.
Aproveitando esse momento de “aha!”, espero ajudar quem ainda está no desafio de entender melhor o que é o sistema de promessas e o que elas têm a ver com a maneira como arquitetamos nossas aplicações.
Atualmente os computadores conseguem fazer “tarefas” (vamos chamá-las assim, é mais fácil) ao mesmo tempo, resultando em processos mais eficientes e rápidos.
Imagine um cenário no qual você conseguisse levantar da cama, fazer café, tomar banho e escovar os dentes ao mesmo tempo; aqueles 45 minutos da manhã para “acordar” agora seriam por volta de 15 minutos.
Para você (humano deste universo) parece impossível, pois precisa primeiro levantar da cama para depois escovar os dentes, por exemplo. Existe uma ordem para que essas tarefas sejam executadas, ou seja, a tarefa de escovar os dentes fica bloqueada, esperando o processo de levantar da cama acabar e que, no meu caso demora, rsrs.
Existe a possibilidade de fazer algumas tarefas ao mesmo tempo. Por exemplo, imagine que minha maquina de café está programada para fazer café no horário em que estou acordando, então uma tarefa como fazer café não fica impedida da tarefa levantar da cama.
Vamos definir dois tipos de tarefas (ou transações):
Síncrona
Tarefa que impede (blocking) você de fazer outra tarefa, pois depende da conclusão de uma terceira tarefa.
// nodejs try { var value = JSON.parse(fs.readFileSync("file.json")); console.log(value.success); } // Syntax actually not supported in JS but drives the point. catch(SyntaxError e) { console.error("invalid json in file"); } catch(Error e) { console.error("unable to read file") }
Assíncrona
Tarefa que não impede (non-blocking) você de realizá-la enquanto outras tarefas são feitas.
// nodejs fs.readFile("file.json", function(error, value) { if ( error ) { console.error("unable to read file"); } else { try { value = JSON.parse(val); console.log(val.success); } catch( e ) { console.error("invalid json in file"); } } });
Trabalhar com processos assíncronos não é simples, mas vale a pena, já que ganhamos muito com a rapidez. Precisamos agora de ferramentas que nos ajudem a enfrentar os desafios que uma aplicação assíncrona traz.
Hoje existem algumas maneiras que os programadores vem adotando.
Sistema de callbacks
Este conceito é considerado, por enquanto, o padrão do NodeJS, mas também é conhecido como “Callbacks Hell”.
function getUserPage(callback) { fetchUsers(function() { renderUsersOnPage(function() { fadeInUsers(function() { loadUserPhotos(function() { // you get the idea… // Indicate the User page is done. callback(null, page); }); }); }); }); } getUserPage(function (error, page) { // render page when its done. });
Você também pode utilizar uma biblioteca chamada Async.js, que ajudará a manter a ordem de execução de cada função, por exemplo:
var users = []; async.series([ fetchUsers, renderUsersOnPage, fadeInusers, loadUserPhotos ]);
Callbacks para realizar tarefas assíncronas nos obrigam a sacrificar alguns dos benefícios de quando programamos tarefas síncronas, como:
- Funções retornam valores ou exceções (erros) quando concluídas.
- Lançar uma excessão (throw) sem precisar usar try e catch prévio.
Essas questões são causadas porque o conceito de callbacks te obriga, de certa maneira, a mudar o workflow conhecido, pois agora você passa a ter funções vazias com erros não localizados, dificultando na hora de debugar e o entendimento do sistema.
Promessas
O surgimento das promessas nos permitiu escrever sistemas assíncronos sem sacrificar features nativas e esperadas pela linguagem, ou seja, escrever código como se fosse síncrono.
Atualmente, graças à comunidade do JavaScript, existe uma especificação chamada “Promises/A” e uma extensão “Promises/A+”, em que se define o que é necessário para uma biblioteca se autoproclamar baseada em promessas. Vale a pena a leitura!
Uma API de promessas vai vir nativamente no novo ES6, então é bom já ir se acostumando com elas, e também ficar atento a nova funcionalidade “yield” que vai possibilitar que bibliotecas como Task.js usem functions generators. Mas chega disso e vamos focar nas promessas por agora.
Uma boa definição, feita por Kris Kowal na JSJ, é:
A promise is an abstraction for asynchronous programming. It’s an object that proxies for the return value or the exception thrown by a function that has to do some asynchronous processing.
Traduzindo, promessa é uma abstração para programação assíncrona, que recebe algum input (dados) e retorna uma promessa do output (resultado) final. Diferente de um sistema baseado em callbacks, que recebe um input e um callback (função), que é executado passando algum output.
Basicamente, uma promessa deve conter o método “then” em três estados:
- Pendente (pending) – Podendo mudar para realizada ou rejeitada.
- Realizada (fulfilled) – Não pode mudar, e precisa retornar um valor/dado/output.
- Rejeitada (rejected) – Não pode mudar, e precisa retornar uma razão pela qual a promessa foi rejeitada.
O método “then” é executado quando a promessa se encontra no estado “fulfilled” (realizada), passando duas funções como argumentos e retornando uma promessa, possibilitando a criação de uma cadeia de promessas (promise chaining), na qual a próxima promessa sempre poderá utilizar os resultados das promessas anteriores:
getUser(21) .then(getStarredRepos(’sebas5384’)) .then(onFulfilled, onRejected);
Vamos supor que precisamos ler vários arquivos ao mesmo tempo:
// nodejs function readFile(filename, enc) { // Return a promise of the result. return new Promise(function (resolve, reject) { // Do the async I/O task, reading the file. fs.readFile(filename, enc, function (error, result) { if (error) { // In case of error, reject the promise passing the reason. reject(error); } else { // All good, resolve the promise returning the result. resolve(result); } }); }); } // Parallel file reading. Promise.all([ readFile('article1.txt', 'UTF-8'), readFile('article2.txt', 'UTF-8'), readFile('article3.txt', 'UTF-8') ]) .done(function (results) { // All the promises where completed. }, function (error) { // Something goes wrong. }); // Or in series, in order, one after the other. readFile('article1.txt', 'UTF-8') .then(readFile('article2.txt', 'UTF-8')) .then(readFile('article3.txt', 'UTF-8')) .then(function (results) { // All the promises where completed. });
Existem várias bibliotecas que podemos usar em JavaScript, e o conceito se aplica a outras linguagens.
Para compor este artigo usei como referência os seguintes:
- Sobre Patterns
- Really?! You can do that with promises?!
- Mozzila Developer Network
- Conformant Implementations
- You’re Missing the Point of Promises
- Why coroutines won’t work on the web
- Callbacks are imperative, promises are functional: Node’s biggest missed opportunity
- task.js
Até o próximo assunto! Se você tiver alguma sugestão, é só deixar nos comentários abaixo 🙂
***
Texto publicado originalmente em http://blog.taller.net.br/o-que-sao-promessas-javascript/