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 este momento de “aha!”, espero ajudar quem ainda está no desafio de entender melhor o que é o sistema de promessas e o que elas tem 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 onde 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 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 obriga a sacrificar alguns dos benefícios de quando programamos tarefas síncronas, como por exemplo:
- 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.
Estas 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 entendimento do sistema.
Promessas em Javascript
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+”onde se define o que é necessário para uma biblioteca se auto proclamar 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 bibliotecas como Task.js usando 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, onde recebe um input e um callback (função), que é executado passando algum output.
Basicamente, uma promessa deve conter o método “then” em 3 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) onde 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 para outras linguagens.
Para compor este artigo usei como referência os seguintes posts:
Really?! You can do that with promises?!
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
Até o próximo assunto! Se você tiver alguma sugestão, é só deixar nos comentários abaixo 🙂