Criando um função construtora tarefas diárias:
function DailyTask() {}
e um método para lidar com a execução dessas tarefas:
DailyTask.perform_sync = function(task) {
return task;
}
Basicamente a implementação é uma função que recebe uma tarefa como argumento e retorna a tarefa executada.
Usando esse método podemos executar tarefas sequencialmente como na maioria das linguagens.
var task_1 = DailyTask.perform_sync('Task 1!');
console.log(task_1); //=> "Task 1!"
var task_2 = DailyTask.perform_sync('Task 2!');
console.log(task_2); //=> "Task 2!"
var task_3 = DailyTask.perform_sync('Task 3!');
console.log(task_3); //=> "Task 3!"
Uma das vantagens do javascript é que podemos executar tarefas assincronamente, assim podemos executar tarefas em "paralelo" com um fluxo de execução não bloqueante, e útil quando precisamos executar diversas tarefas, onde a ordem de execução é indiferente.
Criando uma tentativa de implementação de um metodo que trabalha com estratégia assíncrona, onde a função setTimeout() vai ser o tempo de resposta.
DailyTask.perform_async = function(task) {
setTimeout(function() {
return task;
}, DailyTask.durationEstimate());
}
Também criar um método, que vai tornar o tempo de execução de uma tarefa indeterminado, alguma coisa dentro de 1 segundo.
DailyTask.durationEstimate = function() {
return (
Math.floor(
Math.random(1,1000) * (1000- 1 + 1)) + 1
); // 1..1000;
}
Usando o perform_async:
var task_1 = DailyTask.perform_async('Task 1!');
console.log(task_1); //=> undefined
var task_2 = DailyTask.perform_async('Task 2!');
console.log(task_2); //=> undefined
var task_3 = DailyTask.perform_async('Task 3!');
console.log(task_3); //=> undefined
Nessa implementação, o retorno da função perform_async é imediato, mas o valor é "undefined", porque não sabemos quando será executada a função dentro setTimeout e nem garantia se de fato irá ser executada.
No javascript Funções são cidadões de primeira classe, que é um termo hipster pra dizer que funções suportam diversas operações, assim como outras entidades no javascript. Ou seja, pode ser passada como argumento, usada como retorno de uma outra função, atribuida à uma variavel, etc.
Nessa suruba, podemos implementar um novo método para executar uma tarefa (também em um tempo indeterminado), mas quando de fato a tarefa for executada, esse método nos avise, utilizando uma função que passaremos como argumento(callback).
DailyTask.perform_async_callback = function(task, callback) {
setTimeout(function() {
callback(task)
}, DailyTask.durationEstimate());
}
Com esse novo método podemos executar tarefas e no momento em que elas forem executadas, nossa função(callback) será chamada, e saberemos que a tarefa foi executada com sucesso.
DailyTask.perform_async_callback('Task 1!', function(task_1) {
console.log(task_1);
});
DailyTask.perform_async_callback('Task 2!', function(task_2) {
console.log(task_2);
});
DailyTask.perform_async_callback('Task 3!', function(task_3) {
console.log(task_3);
});
Mas qual é a ordem de execução acima? bom, não sei e tambem nem importa. Mas caso a ordem de execução fosse importante, podemos aninhar tarefas, e quando o callback de uma tarefa for invocado, iniciamos a tarefa seguinte.
DailyTask.perform_async_callback('Task 1!', function(task_1) {
console.log(task_1);
DailyTask.perform_async_callback('Task 2!', function(task_2) {
console.log(task_2);
DailyTask.perform_async_callback('Task 3!', function(task_3) {
console.log(task_3);
});
});
});
Tudo certo num cenário perfeito, mas e se um erro impedir uma tarefa de ser executada? Ainda não estamos preparados para lidar com problemas nessa implementação.
Pra lidar com esses erros, uma padrão adotado é o "error-first callback" onde passamos o erro como primeiro argumento, sempre que invocamos o callback ou deixamos o primeiro parametro nulo, caso não haja erros.
DailyTask.perform_async_callback_with_error = function(task, callback) {
try {
var duration = DailyTask.durationEstimate();
// se a duração for impar, temos um erro
if (duration % 2 == 1) {
throw "I don't wanna do " + task;
}
setTimeout(function() {
// não tem erro, primeiro parametro é nulo,
callback(null, task)
}, duration);
} catch(e) {
// temos um erro, passamos ele como primeiro parametro.
callback(e, null);
}
}
Esse padrão é muito utilizado em muito código open source do nodejs, libs javascript e afins.
Para utilizamos essa novo método, sempre verificamos se um erro foi passado como argumento, antes de seguir com a regra de negócio.
DailyTask.perform_async_callback_with_error('Task 1!', function(error, task_1) {
if (error != null) {
return console.log(error);
}
console.log(task_1);
});
DailyTask.perform_async_callback_with_error('Task 2!', function(error, task_2) {
if (error != null) {
return console.log(error);
}
console.log(task_2);
});
DailyTask.perform_async_callback_with_error('Task 3!', function(error, task_3) {
if (error != null) {
return console.log(error);
}
console.log(task_3);
});
Pode ficar ainda mais bonito quando aninhamos tarefas que devem ser executadas sequencialmente.
DailyTask.perform_async_callback_with_error('Task 1!', function(error, task_1) {
if (error != null) {
return console.log(error);
}
console.log(task_1);
DailyTask.perform_async_callback_with_error('Task 2!', function(error, task_2) {
if (error != null) {
return console.log(error);
}
console.log(task_2);
DailyTask.perform_async_callback_with_error('Task 3!', function(error, task_3) {
if (error != null) {
return console.log(error);
}
console.log(task_3);
});
});
});
Para resolver esse problema, foi adicionado ao JS, o objeto promise, que está disponível no escopo global.
Assim na implementação de uma função, que executa um processamento assíncrono e/ou demorado, Seu retorno imediato pode ser um objeto "promessa", que provê métodos para agir, quando a "promessa" for cumprida(then
) ou lidar com erros(catch
), quando ocorrerem.
Para criar uma promise, é só instanciar a função construtora Promise
, passando uma função como argumento.
new Promise(function(resolve, reject){});
Essa função recebe duas funções como parametro (resolve
, reject
)
resolve()
para ser invocada quando a promessa for executada com sucesso e reject()
em caso de erro.
E conseguimos uma api padrão para lidar com esse tipo de fluxo, e combater o "callback hell".
Implementando promise em novo método que executa tarefas
DailyTask.perform_with_promise = function(task) {
// o retorno do método é uma promise,
// então retornamos uma instancia de Promise.
return new Promise(function(resolve, reject) {
try {
var duration = DailyTask.durationEstimate();
// se a duração for impar, temos um erro
if (duration % 2 == 1) {
throw "I don't wanna do " + task;
}
setTimeout(function() {
// não tem erro invocamos resolve,
// e passamos a tarefa executada como parâmetro.
resolve(task)
}, duration);
} catch(e) {
// se tiver erro invocamos reject,
// e passamos o erro como parâmetro.
reject(e);
}
});
}
E para utilizar esse novo método, como sabemos que uma promise será retornada, podemos utilizar o metodo then
da promise retornada em caso de sucesso e o catch
em caso de erro, e não precisamos mais verificar se tem erro, só implementamos como agir em cada caso.
DailyTask.perform_with_promise('Task 1!')
.then(function(task) {
console.log(task);
})
.catch(function(error) {
console.log(error);
})
Podemos tambem aninhar, se a execução deve ser feita sequencialmente.
DailyTask.perform_with_promise('Task 1!')
.then(function(task) {
console.log(task);
DailyTask.perform_with_promise('Task 2!')
.then(function(task) {
console.log(task);
DailyTask.perform_with_promise('Task 3!')
.then(function(task) {
console.log(task);
})
.catch(function(error) {
console.log(error);
})
})
.catch(function(error) {
console.log(error);
})
})
.catch(function(error) {
console.log(error);
})
Tambem podemos executar em "lote", tratando o erro e sucesso em um unico ponto com o metodo da função Promise.all
Obs.: Se alguma tarefa não for executada(por erro), o erro é lançado no catch()
e o then()
não será chamado.
Promise.all([
DailyTask.perform_with_promise('Task 1!'),
DailyTask.perform_with_promise('Task 2!'),
DailyTask.perform_with_promise('Task 3!'),
DailyTask.perform_with_promise('Task 4!'),
DailyTask.perform_with_promise('Task 5!'),
DailyTask.perform_with_promise('Task 6!'),
DailyTask.perform_with_promise('Task 7!'),
DailyTask.perform_with_promise('Task 8!'),
DailyTask.perform_with_promise('Task 9!')
]).then(function(task) {
// task vai conter um array com todas as tarefas executadas
console.log(task);
}).catch(function(error) {
// erro
console.log(error);
})
Texto muito maneiro Jonas, agora as dúvidas:
A ordem dos parâmetros que as Promises recebem é diferente do padrão "error-first callback", qual a melhor forma de construir funções? Usando o padrão "error-first callback" ou usando o padrão das Promises?