Skip to content

Instantly share code, notes, and snippets.

@jonasporto
Last active September 21, 2016 20:01
Show Gist options
  • Save jonasporto/b50b08a4a47792a0b4e10351c959c030 to your computer and use it in GitHub Desktop.
Save jonasporto/b50b08a4a47792a0b4e10351c959c030 to your computer and use it in GitHub Desktop.
Playing with Promises and Callbacks

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);
})
@marcodearaujo
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment