Tabla de Contenidos

6.2 Servicio $q

El servicio de $q es un servicio de AngularJS que contiene toda la funcionalidad de las promesas. Tal y como se indica en su documentación, está basado en la implementación de Kris Kowal's Q.. AngularJS ha hecho su propia versión para que esté todo integrado en el propio framework.

Yo suelo comparar el sistema de promesas al problema del productor-consumidor. La similitud es que hay una parte que generará la información , por ejemplo el método $http y otra parte que consumirá la información, por ejemplo nuestro código. Esta separación es importante ya que hay 2 objetos con los que tendremos que tratar.

En la nomenclatura de AngularJS al productor se le llama defered y al consumidor se le llama promise. Mediante el servicio de $q obtenemos el objeto defered llamando el método defer() y a partir de él obtenemos el objeto promise llamando a la propiedad promise.

En el siguiente diagrama UML podemos ver las distintas clases que forman parte de las promesas:

PlantUML Graph

Una vez creada mediante el método defer(), una promesa se puede encontrar en alguno de los siguiente 3 estados:

PlantUML Graph

Debido a que la promesa debe acabar en el estado Pendiente o Rechazado es obligatorio que siempre llamemos al método defered.resolve() o defered.reject().

$q.defer()

Para explicar las promesas vamos a hacer un ejemplo de una función que acepta como parámetro dos valores y de forma asíncrona los retorna. Obviamente para hacer una simple suma no es necesario usar promesas ni retornarlo de forma asíncrona, pero servirá como ejemplo.

Lo primero es crear los 2 objetos que se necesitan en las promesas.

function sumaAsincrona(a,b) {
   var defered=$q.defer();
   var promise=defered.promise;
   
   return promise;
}

Ya tenemos los 2 objetos preparados y listos para ser usados. Ahora explicaremos más sobre cada uno de ellos.

defered

El objeto defered sólo se usa desde dentro de la función asíncrona, por lo tanto el que llama a sumaAsincrona no sabe nada del objeto defered. Como ya hemos dicho, el objeto defered hará las funciones de productor de la información 1).

Este objeto tiene 2 métodos. Uno de ellos para indicar que se ha obtenido la información y por lo tanto hacer que la promesa pase al estado “Resuelto” y un segundo método para indicar que algo ha fallado y que no se ha podido obtener la información y por lo tanto hacer que la promesa pase al estado “Rechazado”.

Método Parámetros Descripción
resolve resolve(resultado) Llamaremos a este método para indicar que ya tenemos la información que se solicitó y por lo tanto que la promesa está resuelta, siendo el parámetro resultado el que contiene la información solicitada.
reject reject(error) Llamaremos a este método para indicar que no ha sido posible obtener la información que se solicitó y por lo tanto que la promesa está rechazada, conteniendo el parámetro error información relativa a la naturaleza del error.

Hay que fijarse que el objeto defered no dice nada relativo al tipo de información que se retorna ni a la estructura de la misma. Éso ya dependerá de cada uno de los métodos que creemos que usen promesas. Tampoco se permite retornar más de un dato. Si queremos retornar más de uno simplemente debemos crear un objeto que contenga la información que queramos.

Sigamos ahora con el ejemplo y usemos el servicio de $timeout para simular una respuesta asíncrona de la función.

function sumaAsincrona(a,b) {
   var defered=$q.defer();
   var promise=defered.promise;
   
   $timeout(function() {
      try{
         var resultado=a+b;
         defered.resolve(resultado);
      } catch (e) {
         defered.reject(e);
      }   
   },3000); 
   
   return promise;
}

promise

Acabamos de ver lo que hay que hacer internamente para producir la información en la función sumaAsincrona. Ahora pasemos al otro lado del problema. Veamos qué ocurre al llamar a nuestra función asíncrona, es decir en la parte del consumidor.

Lo primero que debemos hacer es llamar a la función asíncrona y guardarnos la promesa que nos retorna.

var promise=sumaAsincrona(5,2);

¿Cómo podemos saber ahora si se ha podido o no obtener el dato? ¿Y cómo obtenemos el dato? El objeto promise tiene un método llamado then, el cual acepta que le pasemos 2 funciones de callback para saber lo que ha ocurrido. La primera función se llamará si se ha obtenido la información y por lo tanto si la promesa ha sido resuelta. La segunda función 3) se llamará si algo ha fallado y por lo tanto si la promesa ha sido rechazada.

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

Ejemplo

<!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}}
  </body>
</html>

var app = angular.module("app", []);

app.controller("PruebaController", ['$scope', '$q', '$timeout',function($scope, $q, $timeout) {

    $scope.mensaje = "Esperando a que se resuelva la promesa"

    function sumaAsincrona(a, b) {
      var defered = $q.defer();
      var promise = defered.promise;

      $timeout(function() {
        try {
          var resultado = a + b;
          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;
    });

}]);

Normalmente, para abreviar, en vez de guardarnos la variable de la promesa y luego llamar al método then se suele hacer todo junto de la siguiente manera:

    sumaAsincrona(5, 2).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;
    });

Referencias

1) aunque realmente no la produce sino que sólo notifica si se ha producido o no
2) línea 12
3) que llamaremos función de fallo