Tabla de Contenidos

6.3 Promesas avanzadas

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.

PlantUML Graph

Notificaciones

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:

PlantUML Graph

Vemos ahora que se puede llamar a defered.notify() todas las veces que se quiera y la promesa seguirá en el estado Pendiente.

No es posible llamar a 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+"%";
    });

Finalizador

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:");
    });

Hacer una promesa de un valor

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);
      }

Promesas en paralelo

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:

Array de promesas

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:

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+"%";
    });

Objeto con 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:

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+"%";
    });

Esta forma de usar las promesas la volveremos a ver en Resolve

Promesas encadenadas

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.

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.

Recuperarse de un error

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.

Fallo

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;
    });

Por supuesto podría interesarnos saber el paso concreto que ha fallado, en cuyo caso tendremos que añadir tantas funciones de fallo como promesas haya.

Progreso

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+"%";
    });

Por supuesto podría interesarnos saber por separado las notificaciones de cada promesa,y en ese caso tendremos que añadir tantas funciones de notificación como promesas haya.

Ejemplo

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:

Referencias

1) ésto se hace ya que no se puede llamar a notify antes de retornar el objeto de la clase Promise
2) siempre y cuando esté bien programado y siempre se resuelva o rechaze la promesa
3) si es un valor en vez de una promesa se transforma en promesa mediante $q.when.
4) o un valor que se transformará automáticamente en promesa mediante $q.when