Acabamos de ver cómo funcionan las promesas. Para muchas aplicaciones será necesario sólo lo que acabamos de ver, sin embargo, AngularJS proporciona más métodos que nos pueden ser útiles en ciertos casos. Pasemos ahora a ver mas funcionalidades de las promesas.
Lo primero que vamos a hacer es actualizar el diagrama de clases de UML para reflejar todos los nuevos métodos que vamos a explicar.
Es posible que mientras la función asincrona a la que hemos llamado está calculando el resultado nos pueda notificar su propio progreso. Para ello la clase defered
tiene otro método llamado notify
al que le pasamos un valor. Este valor puede ser recogido desde la promesa añadiendo una tercera función de callback al método then
de promise
.
El diagrama de estados de la promesa ahora se modifica de la siguiente forma:
Vemos ahora que se puede llamar a defered.notify()
todas las veces que se quiera y la promesa seguirá en el estado Pendiente
.
defered.notify()
una vez se haya resuelto la promesa pero tampoco antes de que se llame al método promise.then
.Ésto último implica que no se puede llamar dentro de la función antes de retornar el objeto de la clase Promise
.
El ejemplo del tema anterior lo hemos modificado para incluir un nuevo texto con el % de progreso de la operación.
<!DOCTYPE html> <html ng-app="app"> <head> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script> <script src="//code.angularjs.org/1.2.19/i18n/angular-locale_es-es.js"></script> <script src="script.js"></script> </head> <body ng-controller="PruebaController"> {{mensaje}} <br> {{progreso}} </body> </html>
function sumaAsincrona(a, b) { var defered = $q.defer(); var promise = defered.promise; $timeout(function() { defered.notify(0); }, 1); $timeout(function() { defered.notify(33); }, 1000); $timeout(function() { defered.notify(66); }, 2000); $timeout(function() { try { var resultado = a + b; defered.notify(100); defered.resolve(resultado); } catch (e) { defered.reject(e); } }, 3000); return promise; } var promise = sumaAsincrona(5, 2); promise.then(function(resultado) { $scope.mensaje = "El resultado de la promesa es:" + resultado; }, function(error) { $scope.mensaje = "Se ha producido un error al obtener el dato:" + error; }, function(progreso) { $scope.progreso = progreso+"%"; });
defered.resolve
ya que, de lo contrario, nunca se notificaría.then
la cual será llamada cada vez que nos notifiquen algo sobre el progreso de la operación. En este caso añadimos el carácter ”%” y lo ponemos en el %scope
para que se muestre en la página HTML.
Hay veces que nos interesa que se ejecute un código cuando se resuelve la promesa independientemente de si ha sido resuelta o rechazada. Para ello la clase Promise
dispone de un método llamado finally
que acepta como argumento una función de callback. Esta función de callback se llamará justo antes de llamar a las funciones definidas en el método then
.
Debido a que el método se llama finally
y ésta es una palabra reservada en javaScript, puede que el navegador no te deje llamarla directamente así que se deberá llamar usando la siguiente forma:
promise["finally"](function() { alert("Mensaje del ejemplo.Ésto se llama justo antes de resolver o rechazar la promesa:"); });
Otro caso que puede ocurrir es que la función a la que llamamos en vez de tener que calcular asíncronamente el resultado ya lo tenga disposible. Un caso muy típico es una llamada $http
en el que el resultado está cacheado. En ese caso no tendría sentido usar una promesa puesto que ya tenemos el resultado en el momento de llamar a la función. Sin embargo, como el interfaz de nuestra función no lo podemos modificar , es necesario seguir usando una promesa.
Para estos casos AngularJS ofrece el método $q.when
que permite transforma cualquier valor en una promesa.
Siguiendo nuestro ejemplo hemos modificado la función sumaAsincrona
para que cuando ambos argumentos son cero, no sea necesario hacer el “calculo” sino directamente retornamos el cero.
if ((a===0) && (b===0)) { return $q.when(0); }
Otra método útil de $q
es all
. Este método permite retornar una única promesa que unifica a varias promesas. Esto se utiliza para cuando queremos que varias promesas se ejecuten en paralelo y queremos esperar hasta que todas ellas estén resueltas.
Hay 2 formas de hacer ésto:
Lo que hacemos es obtener todas las promesas y añadirlas todas a un array. Entonces llamamos al método $q.all(array)
y le pasamos el array que contiene todas las promesas. Este método nos retornará la promesa unificada, la cual sólo se resolverá si todas se resuelven. Si alguna de las promesas iniciales es rechazada, la promesa unificada también será rechazada.
Al haber varias promesas la forma de funcionar cambia de la siguiente forma:
$q.when
para transformar el valor en una promesa.En el siguiente ejemplo podemos ver cómo se unifican varias promesas en una sóla usando un array:
$scope.mensajeMultipleArray = "Esperando a una promesa múltiple formada por muchas promesas en forma de Array"; var promesaMultipleArray=$q.all([sumaAsincrona(0,0),sumaAsincrona(1,0),sumaAsincrona(2,0)]); promesaMultipleArray.then(function(resultado) { $scope.mensajeMultipleArray = "El resultado de la promesa múltiple formada por muchas promesas en forma de Array es:" + resultado[0] + "," + resultado[1] + "," + resultado[2]; }, function(error) { $scope.mensajeMultipleArray ="Se ha producido un error en alguna de las multiples promesas:"+error; }, function(progreso) { $scope.progresoMultipleArray = progreso+"%"; });
promesaMultiple
que es un array de la unión de las 3 promesas .resultado
es un array con los 3 resultados de las 3 promesas.
Lo que hacemos es obtener todas las promesas y añadirlas todas a un objeto, donde cada propiedad del objeto debe ser una promesa. Entonces llamamos al método $q.all(objeto)
y le pasamos el objeto que contiene las promesas. Este método nos retornará la promesa unificada la cual sólo se resolverá si todas se resuelven. Si alguna de las promesas iniciales es rechazada, la promesa unificada también será rechazada.
Al haber varias promesas la forma de funcionar cambia de la siguiente forma:
$q.when
para transformar el valor en una promesa.En el siguiente ejemplo podemos ver cómo se unifican varias promesas en una sola usando un objeto:
$scope.mensajeMultipleObjetos = "Esperando a una promesa múltiple formada por muchas promesas en forma de Objeto"; var promesaMultipleObjetos=$q.all({ promesaA : sumaAsincrona(0,0), promesaB : sumaAsincrona(1,0), promesaC : sumaAsincrona(2,0) }); promesaMultipleObjetos.then(function(resultado) { $scope.mensajeMultipleObjetos = "El resultado de la promesa múltiple formada por muchas promesas en forma de Objeto es:" + resultado.promesaA + "," + resultado.promesaB + "," + resultado.promesaC; }, function(error) { $scope.mensajeMultipleObjetos ="Se ha producido un error en alguna de las multiples promesas:"+error; }, function(progreso) { $scope.progresoMultipleObjetos = progreso+"%"; });
mensajeMultipleObjetos
que es la unión de las 3 promesas. Cada promesa es un propiedad del objeto que hemos creado.resultado
es un objeto con los 3 resultados de las 3 promesas. Cada propiedad coincide con el nombre de la propiedad del objeto que contenía las promesas.
El último tema que queda por ver es la posibilidad de encadenar resultados de promesas.Como vimos al principio de esta unidad, es lo más importante de las promesas ya que permite evitar la pirámide de la muerte.
La forma de conseguir ésto es una nueva funcionalidad del método then
de las promesas. Resulta que las 2 primeras funciones de callback del método then
permiten que se retorne una promesa 3) y entonces dicha promesa la retorna el método then
.
En principio no parece que ésto sea una gran avance pero veamos un ejemplo de cómo funciona.
El ejemplo consiste en gracias a la función sumaAsincrona
calcular la potencia de (2^4) es decir el valor 16. Para ello llamamos a sumaAsincrona
4 veces sumando a sí mismo el valor de la llamada anterior.
sumaAsincrona(1,1)
lo que da un resultado de 2. Usaremos el resultado para la siguiente llamada.sumaAsincrona(2,2)
lo que da un resultado de 4. Usaremos el resultado para la siguiente llamada.sumaAsincrona(4,4)
lo que da un resultado de 8. Usaremos el resultado para la siguiente llamada.sumaAsincrona(8,8)
lo que da un resultado de 16. Ya tenemos el resultado.
Como vemos cada llamada a sumaAsincrona
se hace con el resultado de la llamada anterior, por lo tanto las llamadas están encadenadas.
Si vemos el código siguiente, gracias a las promesas no es necesario tener función de callback que dentro tiene otra función de callback y así sucesivamente sino que son funciones de callback independientes unas de otras. Sí están relacionadas pero no es necesario anidarlas.
$scope.mensajeEncadenada="Esperando el resultado de promesas encadenadas, esto tardará 12 segundos..... "; var promesasEncadenadas=sumaAsincrona(1,1); promesasEncadenadas.then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { $scope.mensajeEncadenada="El resultado de las promesas encadenadas es:" + resultado; });
Cada función de callback que resuelve la promesa a su vez retorna otra promesa. Ese objeto promesa se retorna en la función then
lo que permite encadenar otras funciones de callback y así todas las veces que queramos sin que esté ninguna función de callback anidada con ninguna otra.
Hemos visto cómo se generan las llamadas si todo funciona correctamente, si falla alguna función se detendrá la cadena de llamadas y ya está . Pero hay una forma de poder tratar el error y seguir con la cadena de llamadas. Podemos añadir una función de callback para cuando se produce un error y si dicha función de error retorna una promesa 4) se seguirá la cadena de llamadas como si nada hubiera fallado.
Vamos a modificar nuestro ejemplo para incluir una función en caso de que algo falle en la segunda llamada.
$scope.mensajeEncadenada="Esperando el resultado de promesas encadenadas, esto tardará 12 segundos..... "; var promesasEncadenadas=sumaAsincrona(1,1); promesasEncadenadas.then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { return sumaAsincrona(resultado,resultado); },function(error) { return 4; }).then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { $scope.mensajeEncadenada="El resultado de las promesas encadenadas es:" + resultado; });
El ejemplo es un poco naíf ya que en caso de fallo retornamos directamente el valor correcto. En un caso real sería más complejo o quizás imposible corregir el fallo y retornar otra promesa. Pero aún así sirve para explicar cómo la función de fallo permite seguir con la cadena de promesas.
A diferencia del ejemplo anterior no es normal que podemos arreglar el resultado cuando algo ha fallado sino que simplemente queremos enterarnos si ha fallado algo de la cadena de promesas. AngularJS permite de una forma sencilla enterarnos si algo ha fallado. Simplemente ponemos una función de fallo en la última promesa y si alguna de ellas falla se llamará a dicha función.
$scope.mensajeEncadenada="Esperando el resultado de promesas encadenadas, esto tardará 12 segundos..... "; var promesasEncadenadas=sumaAsincrona(1,1); promesasEncadenadas.then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { return sumaAsincrona(resultado,resultado); },function(error) { return 4; }).then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { $scope.mensajeEncadenada="El resultado de las promesas encadenadas es:" + resultado; },function(error) { $scope.mensajeEncadenada="Se ha producido un error en alguna de las promesas:"+error; });
Otra característica que tienen las promesas encadenadas es que permiten también añadir una función de callback en la última promesa por lo que llamará para todas las promesas de la cadena.
El ejemplo lo volvemos ahora a modificar añadiendo dicha función:
$scope.mensajeEncadenada="Esperando el resultado de promesas encadenadas, esto tardará 12 segundos..... "; var promesasEncadenadas=sumaAsincrona(1,1); promesasEncadenadas.then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { return sumaAsincrona(resultado,resultado); },function(error) { return 4; }).then(function(resultado) { return sumaAsincrona(resultado,resultado); }).then(function(resultado) { $scope.mensajeEncadenada="El resultado de las promesas encadenadas es:" + resultado; },function(error) { $scope.mensajeEncadenada="Se ha producido un error en alguna de las promesas:"+error; },function(progreso) { $scope.progresoEncadenada=progreso+"%"; });
El ejemplo contiene llamadas a todas los métodos que hemos visto en este tema por lo que el código JavaScript ha quedado un poco largo, y se ve lo siguiente: