En este tutorial vamos a aprender a trabajar asíncronamente en AngularJS con el patrón promise-deferred de la mano de Samuel Martín.
Según vamos trabajando en aplicaciones mas complejas, se hace necesario el uso de funciónes asíncronas para que ejecuten procesos costosos en segundo plano sin penalizar la experiencia de usuario, como por ejemplo en el caso de llamadas a servidor.
Para trabajar asíncronamente hacemos uso de los callbacks, que, según la definición de la Wikipedia, son piezas de código ejecutable que se pasan como argumentos a otro código. este último es el encargado de ejecutar este argumento cuando sea posible. En una llamada síncrona esta invocación es inmediata mientras que puede ser posterior en una llamada asíncrona.
Para clarificar este tutorial voy a usar algunos ejemplos. De toda la documentación de la que me he servido para la creación de este tutorial me han parecido muy claros los ejemplos de Rodrigo Branas en su libro AngularJS Essentials, por lo que voy a utilizarlos para explicar las promesas en AngularJS.
Una primera implementación de un servicio ficticio de búsqueda de coches llamado ‘carSearchService’ usa callbacks para devolver los resultados al controlador cuando la busqueda del coche ha finalizado.
serviceCallback.js
parking.factory('carSearchService', function ($timeout) { var _filter = function (cars, criteria, successCallback, errorCallback) { $timeout(function () { var result = []; angular.forEach(cars, function (car) { if (_matches(car, criteria)) { result.push(car); } }); if (result.length > 0) { successCallback(result); } else { errorCallback("No se han encontrado resultados"); } }, 1000); }; var _matches = function (car, criteria) { return angular.toJson(car).indexOf(criteria) > 0; }; return { filter: _filter } });
Para llamar a esta funcionalidad del servicio, necesitamos pasarle ambos callbacks, lo haremos en el controlador.
controladorCallback.js
var criteria = 'ferrari'; $scope.cars = ['ford','ferrari']; $scope.searchCarsByCriteria = function (criteria) { carSearchService.filter($scope.cars, criteria, function (result) { $scope.searchResult = result; }, function (message) { $scope.message = message; }); };
Como vemos, desde nuestro controlador le estamos pasando cuatro parámetros. Los datos donde buscar, el criterio de la búsqueda, y dos funciones anónimas. Estas funciones son los callbacks que se ejecutarán cuando se resuelva la promesa, ya sea porque se ha encontrado el coche
o no.
El problema principal con este tipo de implementación es que es complicado mantener un número creciente de callbacks que a mas inri pueden estar anidados. Este problema ha sido recurrente en multitud de proyectos, por lo que se han establecido unos patrones conocidos como deferred y promise que tenemos implementados en AngularJS. Gestionan estos problemas y cuentan con la ventaja añadida de tener una interfaz común, lo que hace mas sencillo entender código legacy, ya que simplemente conociendo el API puedes trabajar con ellos.
Deferred
Para crear una nueva promesa con AngularJS necesitamos inyectar el servicio $q y llamar a su método defer para crear una instancia del objeto deferred, que es como se llama al objeto promesa.
De su API, las funciones mas importantes son resolve(resultado) para, como su propio nombre indica, resolver la promesa y reject(razon), para rechazar la promesa por el motivo que sea.
Vamos a ver otra vez el servicio anterior, pero esta vez utilizando deferred:
servicioDeferred.js
parking.factory('carSearchService', function ($timeout, $q) { var _filter = function (cars, criteria) { var deferred = $q.defer(); $timeout(function () { var result = []; angular.forEach(cars, function (car) { if (_matches(car, criteria)) { result.push(car); } }); if (result.length > 0) { console.log('promesa resuelta') deferred.resolve(result); } else { console.log('promesa rechazada') deferred.reject("No se han encontrado resultados"); } }, 1000); return deferred.promise; }; var _matches = function (car, criteria) { return angular.toJson(car).indexOf(criteria) > 0; }; return { filter: _filter } });
En este caso ya no es necesario pasarle como parámetros los callbacks.
Las partes importantes de este fragmento de código son la asignación de $q.defer() a una variable con nuestra promesa y
los métodos resolve y reject de nuestra promesa de los que hemos hablado antes.
Promise
Como estamos devolviendo un objeto deferred, en nuestro controlador vamos a poder utilizar los metodos then, catch y finally.
- then(successCallback, errorCallback, notifyCallback) es invocado cuando la promesa es resuelta (.resolve).
- catch(errorCallback) se invoca cuando una promesa ha fallado, es equivalente al comportamiento en caso de .then(null, errorCallback).
- finally(callback) al igual que en otros lenguajes como Java, es un fragmento que se va a invocar independientemente de cual sea el resultado de la promesa.
Vamos a ver como debe ser el controlador encargado de llamar al servicioDeferred:
controladorDeferred.js
var criteria = 'ferrari'; $scope.cars = ['ford','ferrari']; scope.filterCars = function (criteria) { carSearchService.filter($scope.cars, criteria) .then(function (result) { console.log('la promesa se ha resuelto'); $scope.searchResults = result; }) .catch(function (message) { console.log('la promesa se ha rechazado'); $scope.message = message; }); };
Mediante el .then y el .catch controlamos la salida de la promesa y mientras esta se resuelve podemos ejecutar otro código importante para el usuario como el renderizado de la página u otras llamadas a servicios.
Existe demasiada gente que programa simplemente gracias a fragmentos de código que va encontrando sin entender demasiado como funciona por dentro, esto puede tener consecuencias desastrosas si el código que están tocando es vital para alguna organización.
Espero haber ayudado con mi granito de arena a que se entienda mejor el funcionamiento de las promesas, en este caso con el framework Angular de JavaScript.
Un saludo,
Samuel Martín Gómez-Calcerrada
Gracias, muy claro el articulo!
Muy útil y muy bien explicado, felicidades y gracias Samuel.