10. Promesas de resultados

Una promesa, o deferred object, es una herramienta muy sencilla y útil cuando realizamos programación asíncrona.

Aunque hay muchas implementaciones en JavaScript, el equipo de AngularJS realizó una adaptación de la librería Q, de Kris Kowal, debido a su éxito y difusión.

10.1. Por qué utilizar promesas

En JavaScript, los métodos asíncronos normalmente utilizan funciones de callback para devolver un estado de éxito o error. Por ejemplo, la API de gelocalización requiere de estas dos funciones de callback para obtener la posición actual:

1 2 3 4 5 6 7 8 9
var successFn = function(response){ console.log('SUCCESS! ' + JSON.stringify(response)); }; var errorFn = function(err) { console.log('ERROR! ' + JSON.stringify(response)); } navigator.geolocation.getCurrentPosition(successFn, errorFn);

Otro ejemplo es el objeto XMLHttpRequest, que utilizamos para realizar peticiones AJAX. Tiene una función de callback llamada onReadyStateChange, que se llama cuando cambia el atributo readyState.

1 2 3 4 5 6 7 8 9 10
var xhr = new window.XMLHttpRequest(); xhr.open('GET', 'http://www.webdeveasy.com', true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { console.log('Success'); } } }; xhr.send();

En nuestro día a día nos encontraremos con una infinidad de usos y ejemplos. Aunque puede parecer sencillo de manejar, puede volverse un infierno cuando tenemos que encadenar funciones de sincronización.

callbackHell

La imagen ilustra un ejemplo de la famosa callback pyramid of doom. Aunque hay maneras más elegantes de escribir y refactorizar el código, siempre será difícil de leer y mantener.

10.2. Promesas y deferred objects

Una promesa representa el resultado de una operación asíncrona. Expone una interfaz que puede usarse para interactuar con el resultado que tendrá dicha operación. Así, también permite que quien esté interesado pueda hacer uso de dicho resultado.

La promesa está asociada a un deferred object, cuyo estado será "pendiente", y no tiene ningún resultado. Cuando invoquemos los métodos resolve() o reject(), este estado pasará a "resuelto" o "rechazado". Además, podemos coger la promesa una vez inicializada y definir operaciones con su resultado futuro, que se llevarán a cabo cuando se cambie a los estados "resuelto" o "rechazado" que acabamos de mencionar.

Mientras el deferred object tiene métodos para cambiar el estado de una operación, la promesa sólo expone métodos para operar con el resultado. Es por ello que es una buena práctica devolver una promesa y no un deferred object.

10.3. Promesas en AngularJS

En primer lugar, deberemos crear un deferred object:

1
var deferred = $q.defer();

El objeto deferred apunta a un deferred object, cuyo estado podremos resolver o rechazar tras realizar una operación asíncrona. Supongamos el método asíncrono async(successFn, errorFn), donde los dos parámetros son funciones de callback. Cuando async finaliza su ejecución, queremos resolver o rechazar deferred con su resultado:

1 2 3 4 5 6 7 8
async( function(val){ deferred.resolve(val); }, function(err){ deferred.reject(err); } );

Incluso podemos simplificar la llamada, ya que los métodos resolve y reject no precisan de un contexto:

1
async(deferred.resolve(val), deferred.reject(err));

Ahora, asignar operaciones una vez haya habido éxito o error es bastante sencillo:

1 2 3 4 5 6 7
var promise = deferred.promise; promise .then( function(data){ alert('Success! ' + data); }, function(data){ alert('Error! ' + data); } )

Podemos asignar tantas funciones de éxito o error como queramos. En el siguiente ejemplo, tanto las funciones asignadas antes de la llamada a async como las que se realizan después se ejecutarán al resultado:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
var deferred = $q.defer(); deferred.promise .then(function(data) { console.log('Success asignado antes de invocar async()', data); }, function(error) { console.log('Error asignado antes de invocar async()', error); }); async(deferred.resolve, deferred.reject); deferred.promise .then(function(data) { console.log('Success asignado tras invocar async()', data); }, function(error) { console.log('Error asignado tras invocar async()', error); });

Hemos visto que el método then recibe dos funciones, una de éxito y una de error. Sin embargo, podemos usar then para asignar funciones de éxito, y utilizar catch cuando se produce un error. Además, existe una función finally, que se ejecutará siempre, se haya resuelto correctamente o no. Gracias a finally, no tendremos que duplicar código que se podría ejecutar tanto en la parte del éxito como la del error.

1 2 3 4 5
deferred.then(function(){ console.log('All OK')}); deferred.catch(function(){ console.log('Error!')}); deferred.finally(function(){ console.log('End.'); });

10.4. Encadenando promesas

Un dato interesante que hay que conocer, es que el método then de una promesa devuelve otra promesa. Cuando resolvemos la primera promesa, el valor que devolvamos se enviará a la promesa siguiente, de manera que podemos encadenar y transformar una serie de promesas de resultados. Veámoslo con un ejemplo concreto:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
function async(value) { // Supongamos que se trata de una operación asíncrona de verdad var deferred = $q.defer(); var asyncCalculation = value / 2; deferred.resolve(asyncCalculation); return deferred.promise; } var promise = async(8) .then(function(x) { return x+1; }) .then(function(x) { return x*2; }) .then(function(x) { return x-1; }); promise.then(function(x) { console.log(x); });

La promesa empieza con llamando a async(8), que resuelve con el valor 4. Este valor pasa por todas las funciones then secuencialmente, hasta pintar el valor 9, ya que hace (8 / 2 + 1) * 2 - 1.

Como hemos visto antes que las funciones no necesitan contexto, podemos refactorizarlo de la siguiente manera:

1 2 3 4 5
var promise = async(8) .then(addOne) .then(mult2) .then(minusOne) .then(paintValue);

El ejemplo es muy optimista y asume que todo va a ir bien. Pero si no es así, ¿dónde colocamos nuestro catch? Bien, en caso de operaciones encadenadas, catch y finally se colocan en último lugar:

1 2 3 4 5 6 7
var promise = async(8) .then(addOne) .then(mult2) .then(minusOne) .then(paintValue) .catch(showError) .finally(endFn);

En el momento que cualquier elemento resuelva incorrectamente (ya sea el primero o el tercero), se ejecutará a continuación el catch, y se terminará con el finally.

10.5. Otros métodos útiles

10.5.1. $q.reject

En algunas ocasiones, puede que necesitemos devolver una promesa rechazada. En lugar de crear una promesa y rechazarla, podemos usar $q.reject(reason), que devuelve una promesa rechazada, con el motivo que le digamos. Por ejemplo:

1 2 3 4 5 6 7 8 9 10
var promise = async() .then( function(value){ if(isValid(value)) { return value; } return $q.reject('Valor no válido'); } );

Si value es válido, un conjunto de promesas encadenadas funcionará correctamente. Sin embargo, se irá al bloque catch si no es válido.

10.5.2. $q.when

Similar a $q.reject, pero devuelve un valor correctamente resuelto. Un ejemplo muy claro de uso es cuando tenemos que pedir un dato al servidor si no lo tenemos cacheado.

1 2 3 4 5 6 7
function getElement(key){ if(!!$localStorage.key) { return $q.when($localStorage.key); } else { return getFromServer(key); } }

10.5.3. $q.all

En algunas ocasiones podríamos querer tener una serie de elementos de manera asíncrona, sin importarnos el orden, y ser notificados al terminar. Para ello, hacemos uso de $q.all(promisesArray). Devuelve una promesa que se resuelve sólo cuando todas las promesas del array se han resuelto. Si al menos una de las promesas del array se rechaza, también lo hará el resultado de $q.all.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
var allPromises = $q.all([ async1(), async2(), async3(), ... asyncN() ]); allPromises.then(function(values){ var value1 = values[0]; var value2 = values[1]; var value3 = values[2]; ... var valueN = values[N+1]; console.log('end'); });

10.6. Ejercicio (0.5 puntos)

Genera un servicio en AngularJS que haga uso de promesas y de la API de geoposicionamiento para devolver las coordenadas del navegador. Haz también un programa en AngularJS que pinte las coordenadas en pantalla.

Aplica el tag promises a la versión que quieres que se corrija.