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>

  • Línea 11: Mostramos el % de progreso de la operación.

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

  • Lineas 6,7 y 8: Se ha puesto un timeout para que al pasar un milisegundo se notifique que vamos por el 0% 1).
  • Líneas 9,10 y 11: Se notifica que vamos por el 33% al pasar 1000 ms.
  • Líneas 12,13 y 14: Se notifica que vamos por el 66% al pasar 2000 ms.
  • Línea 19: Ya hemos acabado los cálculos y notificamos que vamos por el 100%. Recuerda que esta línea hay que ponerla antes de defered.resolve ya que, de lo contrario, nunca se notificaría.
  • Líneas 34 y 35: Se añade la tercera función al método 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.

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

  • Línea 2: Se ejecutará justo antes de resolver o rechazar la promesa , por lo que siempre 2) se ejecutará.

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

  • Línea 2: Retornamos una promesa cuyo resultado ya es cero.

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:

  • Al resolver la promesa, el parámetro es un array con cada uno de los valores.
  • Si alguna de las promesas iniciales es rechazada , a la promesa unificada sólo le llega un único rechazo que será el de la primera promesa que haya sido rechazada. El resto de los rechazos no se notificarán.
  • No se generan mensajes de notificación en la promesa unificada.
  • Si alguno de los elementos del array de promesas no es una promesa , entonces se llamará a $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+"%";
    });

  • Línea 2: Creamos una nueva promesa llamada promesaMultiple que es un array de la unión de las 3 promesas .
  • Línea 5: El argumento resultado es un array con los 3 resultados de las 3 promesas.
  • Línea 7: Solo será llamado una única vez para la primera promesa que sea rechazada.
  • Línea 9: Nunca se llamará a este código ya que nunca se notifica nada en las promesas múltiples.

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:

  • Al resolver la promesa, el parámetro es un objeto cuyas propiedades son cada uno de los valores.
  • Si alguna de las promesas iniciales es rechazada , a la promesa unificada sólo le llega un único rechazo, que será el de la primera promesa que haya sido rechazada. El resto de los rechazos no se notificarán.
  • No se generan mensajes de notificación en la promesa unificada.
  • Si alguna de las propiedades del objeto de promesas no es una promesa , entonces se llamará a $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+"%";
    });

  • Línea 2: Creamos una nueva promesa llamada mensajeMultipleObjetos que es la unión de las 3 promesas. Cada promesa es un propiedad del objeto que hemos creado.
  • Línea 9: El argumento 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.
  • Línea 11: Sólo será llamado una única vez para la primera promesa que sea rechazada.
  • Línea 13: Nunca se llamará a este código ya que nunca se notifica nada en las promesas múltiples.

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.

  • Llamar a sumaAsincrona(1,1) lo que da un resultado de 2. Usaremos el resultado para la siguiente llamada.
  • Llamar a sumaAsincrona(2,2) lo que da un resultado de 4. Usaremos el resultado para la siguiente llamada.
  • Llamar a sumaAsincrona(4,4) lo que da un resultado de 8. Usaremos el resultado para la siguiente llamada.
  • Llamar a 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.

  • Línea 2: Se genera la primera promesa.
  • Línea 5: En esta función de callback se coge el resultado que es 2 y se suma a sí mismo y se retorna otra promesa.
  • Línea 7: En esta función de callback se coge el resultado que es 4 y se suma a sí mismo y se retorna otra promesa.
  • Línea 9: En esta función de callback se coge el resultado que es 8 y se suma a sí mismo y se retorna otra promesa.
  • Línea 11: Finalmente en la última función de callback se muestra el resultado que es de 16.

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

  • Línea 8: Se añade la función de callback para cuando algo falla.
  • Línea 9: Arreglamos el error y retornamos el valor que debería ser , que en este caso es un 4.Al retornar el valor se sigue con las promesas encadenadas como si nada hubiera pasado.

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

  • Línea 14: Definimos una función de fallo en la última promesa de la cadena de promesas.
  • Línea 15: Se ejecutará si alguna de las promesas ha fallado.

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

  • Línea 14: Definimos una función de progreso en la última promesa de la cadena de promesas.
  • Línea 15: Se ejecutará cada vez que alguna de las promesas notifica algo.En nuestro ejemplo al haber 4 llamadas se verá : 0% 33% 66% 100%, 0% 33% 66% 100%, 0% 33% 66% 100%, 0% 33% 66% 100%.

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:

  • Notificaciones
  • Función de finalización
  • Valores inmediatos como promesas
  • Promesas en paralelo con arrays
  • Promesas en paralelo con objetos
  • Promesas encadenadas

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
unidades/06_promesas/03_avanzado.txt · Última modificación: 2014/09/02 00:16 por admin
Ir hasta arriba
CC Attribution-Share Alike 3.0 Unported
chimeric.de = chi`s home Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0