8. Promesas
Las promesas son una característica novedosa para gestionar los eventos asíncronos, permitiendo escribir código más sencillo, callbacks más cortos, y mantener la lógica de la aplicación de alto nivel separada de los comportamientos de bajo nivel.
Al utilizar promesas, podemos usar callbacks en cualquier situación, y no solo con eventos. Además ofrecen un mecanismo estándar para indicar la finalización de tareas.
8.1. ¿Qué es una promesa?
Una promesa es un objeto que representa un evento único, normalmente como resultado de una tarea asíncrona como una llamada AJAX. Almacenan un valor futuro, ya sea el resultado de una petición HTTP o la lectura de un fichero desde disco.
En la vida real, cuando vamos a un restaurante de comida rápida y pedimos un menú con hamburguesa con bacón y patatas fritas, estamos obteniendo una promesa con el número del pedido, porque primero lo pagamos y esperamos recibir nuestra comida, pero es un proceso asíncrono el cual inicia una transacción. Mientras esperamos a que nos llamen con nuestra sabrosa hamburguesa, podemos realizar otras acciones, como decirle a nuestra pareja que busque una mesa o preparar un tuit con lo rica que está nuestra hamburguesa. Estas acciones se traducen en callbacks, los cuales se ejecutarán una vez se finalice la operación. Una vez nuestro pedido está preparado, nos llaman con el número del mismo y nuestra deseada comida. O inesperadamente, se ha acabado el bacón y nuestra promesa se cumple pero erroneamente. Así pues, un valor futuro puede finalizar correctamente o fallar, pero en ambos casos, finalizar.
8.2. Promise API
Las promesas forman parte del API de JavaScript desde ECMAScript 6, con lo cual es una tecnología novedosa que puede no estar disponible en todos los navegadores. Aún así, las últimas versiones de los navegadores ya lo soportan: http://caniuse.com/#feat=promises
Además de la implementación del estándar ECMAScript 6, jQuery implementa las promesas mediante los Deferreds, y existen librerías de terceros como BlueBird (https://github.com/petkaantonov/bluebird) o Q (https://github.com/kriskowal/q) que cumplen el estándard.
8.2.1. Hola Promesa
Para crear una promesa, utilizaremos el contructor Promise()
, el cual recibe como argumento un callback con dos parámetros, resolver
y rechazar
. Este callback se ejecuta inmediatamente.
Para facilitar la lectura del código, las funciones resolver y rechazar están traducidas. El API de promesas utiliza resolve y reject como alternativa, y además, ofrece métodos auxiliares para realizar estas acciones con dicha nomenclatura.
|
Dentro de la promesa, realizaremos las acciones deseadas y si todo funciona como esperamos llamaremos a resolver
. Sino, llamaremos a rechazar
(mejor pasándole un objeto Error
para capturar la pila de llamadas)
var promesa = new Promise(function(resolver, rechazar) {
var ok;
// código con la llamada async
if (ok) {
resolver("Ha funcionado"); // resuelve p
} else {
rechazar(Error("Ha fallado")); // rechaza p
}
});
Así pues, los parámetros resolver
y rechazar
tienen la capacidad de manipular el estado interno de la instancia de promesa p
. Esto se conoce como el patrón Revealing Constructor (http://blog.domenic.me/the-revealing-constructor-pattern/), ya que el constructor revela sus entrañas pero solo al código que lo construye, el cual es el único que puede resolver o rechazar la promesa. Por ello, cuando le pasamos la promesa a cualquier otro método, éste solo podrá añadir callbacks sobre la misma:
hacerCosasCon(promesa);
8.2.2. Uso básico
Una vez tenemos nuestra promesa, independientemente del estado en el que se encuentre, mediante then(callbackResuelta, callbackRechazada)
podremos actuar en consecuencia dependiendo del callback que se dispare:
promesa.then(
function(resultado) {
console.log(resultado); // "Ha funcionado"
}, function(err) {
console.error(err); // Error: "Ha fallado"
}
);
El primer callback es para el caso completado, y el segundo para el rechazado. Ambos son opcionales, con lo que podemos añadir un callback para únicamente el caso completado o rechazado.
La especificación describe el término thenable para aquelos objetos que son similares a una promesa, ya que contienen un método then. |
Uno de los ejemplos más sencillos es una petición AJAX mediante jQuery:
var p = $.get("http://www.omdbapi.com/?t=Interstellar&r=json");
p.then(function(resultado) {
console.log(resultado);
});
8.2.3. Estados de una promesa
Nada más comenzar, una promesa está en un estado pendiente (pending). Al finalizar, su estado será completada (fulfilled), lo que implica que la tarea se ha realizado/resuelto, o rechazada (rejected), si la tarea no se completó.
No hay que confundir completada con éxito, ya que una promesa se puede completar pero contener un error (por ejemplo, el usuario solicitado no existe), o puede no completarse porque el usuario cancelase la petición incluso sin ocurrir ningún error. |
Una vez que una promesa se completa o se rechaza, se mantendrá en dicho estado para siempre (conocido como settled), y sus callbacks nunca se volverán a disparar. Tanto el estado como cualquier valor dado como resultado no se pueden modificar.
Dicho de otro modo, una promesa sólo puede completarse o rechazarse una vez. No puede hacerlo dos veces, ni cambiar de un estado completado a rechazado, o viceversa.
Autoevaluación
A partir del siguiente código anterior, ¿Qué mensajes saldrán por pantalla? [1]
|
Es decir, se queda en un estado inmutable, el cual se puede observar tantas veces como queramos. Por ello, una vez resuelta, podemos pasar tranquilamente la promesa a cualquier otra función, ya que nadie va a poder modificar su estado.
Y ahora viene la pregunta del millón: Si la función rechazar pasa una promesa al estado rechazado, ¿por qué la función resolver no pasa la promesa al estado resuelta en vez de al estado completada? La respuesta corta es que resolver una promesa no es lo mismo que completarla. La larga es que cuando a la función resolver
se le pasa un valor, la promesa se completa automáticamente. En cambio, cuando se le pasa otra promesa (por ejemplo promesa.resolver(otraPromesa)
), las promesas se unen para crear una única promesa, de manera que cuando se resuelve la 2ª promesa (otraPromesa
), ambas promesas se resolverán. Del mismo modo, si se rechaza la 2ª promesa, las dos promesas se rechazarán.
El argumento que se pasa a resolver decide el destino de la promesa. Si es un valor, se completa. Si es una promesa, su estado depende de ésta última.
|
Tanto resolver
como rechazar
se pueden llamar sin argumentos, en cuyo caso el valor de la promesa será undefined
.
Finalmente, si queremos crear una promesa que inmediatamente se resuelva o rechace, podemos emplear los métodos Promise.resolve()
o Promise.reject()
:
new Promise(function(resolver, rechazar) {
resolver("forma larga");
});
Promise.resolve("forma corta");
new Promise(function(resolver, rechazar) {
reject("rechazo larga");
});
Promise.reject("rechazo corta");
Estos métodos son útiles cuando ya tienes el elemento que debería resolver o rechazar la promesa.
8.2.4. Consumiendo promesas
Una gran ventaja de emplear promesas es que podemos adjuntar tantos callbacks a una promesa como queramos, los cuales se ejecutarán una vez que la promesa se resuelva o rechace. Es decir, una promesa la pueden consumir varios consumidores.
Veamos un ejemplo completo. Supongamos que al entrar a una aplicación, queremos mostrar la información del usuario tanto en la barra de navegación como en el titulo de la página.
Si no empleasemos promesas, el código sería similar a:
var usuario = {
perfilUsuario: null,
obtenerPerfil: function() {
if (!this.perfilUsuario) {
var xhr = new XMLHttpRequest();
xhr.open("GET","usuario.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
perfilUsuario = JSON.parse(xhr.responseText);
}
};
xhr.send(null);
}
}
};
usuario.obtenerPerfil();
if (usuario.perfilUsuario) { (1)
document.getElementById("navbar").innerHTML = usuario.login;
document.getElementById("titulo").innerHTML = usuario.nombre;
}
1 | Pese a parecer que este código funciona, nunca se rellenarán los datos porque al ser una llamada asíncrona, la variable perfilUsuario estará a null. |
Para que funcione correctamente, el código de rellenar la página se tiene que acoplar con la petición AJAX:
var usuario = {
perfilUsuario: null,
obtenerPerfil: function() {
if (!this.perfilUsuario) {
var xhr = new XMLHttpRequest();
xhr.open("GET","usuario.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
perfilUsuario = JSON.parse(xhr.responseText);
document.getElementById("navbar").innerHTML = perfilUsuario.login; (1)
document.getElementById("titulo").innerHTML = perfilUsuario.nombre;
}
};
xhr.send(null);
}
}
};
usuario.obtenerPerfil();
1 | Ahora pintamos los datos tras recibirlos de manera asíncrona |
Prometiendo AJAX
En cambio, si al obtener los datos AJAX, los envolvemos en una promesa, el código queda más legible y ahora sí que podemos desacoplar la petición AJAX del dibujado HTML:
var usuario = {
promesaUsuario: null,
obtenerPerfil: function() {
if (!this.promesaUsuario) {
this.promesaUsuario = new Promise(function(resolver, rechazar) {
var xhr = new XMLHttpRequest();
xhr.open("GET","usuario.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
resolver(JSON.parse(xhr.responseText));
}
};
xhr.onerror = function() {
rechazar(Error("Error al obtener usuario"));
};
xhr.send(null);
});
}
return this.promesaUsuario;
}
};
var navbar = {
mostrar: function(usuario) {
usuario.obtenerPerfil().then(function(perfil) {
document.getElementById("navbar").innerHTML = perfil.login;
});
}
}
var titulo = {
mostrar: function(usuario) {
usuario.obtenerPerfil().then(function(perfil) {
document.getElementById("titulo").innerHTML = perfil.nombre;
});
}
}
navbar.mostrar(usuario); (1)
titulo.mostrar(usuario);
1 | Da igual si se ha obtenido el usuario. Como se obtiene una promesa, cuando se resuelva será cuando se dibujen los datos |
Así pues, podemos añadir tanto consumidores a una misma promesa conforme necesitamos, los cuales se ejecutarán una vez de resuelva la misma.
Además, podemos añadir más callback cuando queramos, incluso si la promesa ya ha sido resuelta o rechazada (en dicho caso, se ejecutarán inmediatamente).
Devolviendo promesas
¿Y si queremos realizar una acción tras mostrar los datos del usuario? Al tratarse de una tarea asíncrona, necesitamos a su vez, que las operaciones de mostrar los datos también devuelvan una promesa.
Cualquier función que utilice una promesa debería devolver una nueva promesa |
Ahora nuestro código quedará así:
// ...
var titulo = {
mostrar: function(usuario) {
return usuario.obtenerPerfil().then(function(perfil) { (1)
document.getElementById("titulo").innerHTML = perfil.nombre;
});
}
}
1 | Como then devuelve una promesa, a su vez, hacemos que mostrar la propague |
8.2.5. Prometiendo XMLHttpRequest
A día de hoy XMLHttpRequest
no devuelve una promesa. Siendo, probablemente, la operación asíncrona más empleada por su uso en AJAX, el hecho de que devuelva una promesa va a simplicar las peticiones AJAX. Para ello, podemos crear una función que devuelve una promesa del siguiente modo:
function ajaxUA(url) {
return new Promise(function(resolver, rechazar) { (1)
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
resolver(req.response); (2)
} else {
rechazar(Error(req.statusText)); (3)
}
};
req.onerror = function() { (4)
reject(Error("Error de Red"));
};
req.send();
});
}
1 | Devolvemos una promesa |
2 | Si la petición ha ido bien, resolvemos la promesa |
3 | En cambio, si ha fallado, la rechazamos |
4 | Controlamos los posibles errores de red |
Hecho esto, podemos realizar las peticiones del siguiente modo:
then
ajaxUA('heroes.json').then(function(response) {
console.log("¡Bien!", response);
}, function(error) {
console.error("¡Mal!", error);
});
Gracias al encadenamiento de promesas, podemos obtener el texto de una petición AJAX del siguiente modo:
ajaxUA('heroes.json').then(function(response) { (1)
return JSON.parse(response);
}).then(function(response) { (2)
console.log("JSON de Heroes:", response);
});
1 | Recibe la respuesta JSON |
2 | Recibe el objeto JavaScript parseado |
Además, como JSON.parse
recibe un único argumento y devuelve el texto transformado, podemos simplicarlo del siguiente modo:
ajaxUA('heroes.json').then(JSON.parse).then(function(response) {
console.log("JSON de Heroes:", response);
});
8.2.6. Encadenando promesas
Acabamos de ver que tanto then
como catch
devuelven una promesa, lo que facilita su encadenamiento, aunque no devuelven una referencia a la misma promesa. Cada vez que se llama a uno de estos métodos se crea una nueva promesa y se devuelve, siendo estas dos promesas diferentes:
var p1,p2;
p1 = Promise.resolve();
p2 = p1.then(function() {
// ....
});
console.log(p1 !== p2); // true
Gracias a esto podemos encadenar promesas con el resultado de un paso anterior:
paso1().then(
function paso2(resultadoPaso1) {
// Acciones paso 2
}
).then(
function paso3(resultadoPaso2) {
// Acciones paso 3
}
).then(
function paso4(resultadoPaso3) {
// Acciones paso 3
}
)
Cada llamada a then
devuelve una nueva promesa que podemos emplear para adjuntarla a otro callback. Sea cual sea el valor que devuelve el callback resuelve la nueva promesa. Mediante este patrón podemos hacer que cada paso envíe su valor devuelto al siguiente paso.
Si un paso devuelve una promesa en vez de un valor, el siguiente paso recibe el valor empleado para completar la promesa. Veamos este caso mediante un ejemplo:
Promise.resolve('Hola!').then(
function paso2(resultado) {
console.log('Recibido paso 2: ' + resultado);
return 'Saludos desde el paso 2'; // Devolvemos un valor
}
).then(
function paso3(resultado) {
console.log('Recibido paso 3: ' + resultado); // No devolvemos nada
}
).then(
function paso4(resultado) {
console.log('Recibido paso 4: ' + resultado);
return Promise.resolve('Valor completado'); // Devuelve una promesa
}
).then(
function paso5(resultado) {
console.log('Recibido paso 5: ' + resultado);
}
);
// Consola
// "Recibido paso 2: Hola!"
// "Recibido paso 3: Saludos desde el paso 2"
// "Recibido paso 4: undefined"
// "Recibido paso 5: Valor completado"
Al devolver un valor de manera explícita como en el paso dos, la promesa se resuelve. Como el paso 3 no devuelve ningún valor, la promesa se completa con undefined
. Y el valor devuelto por el paso 4 completa la promesa que envuelve el paso 4.
8.2.7. Orden de ejecución de callbacks
Los promesas permiten gestionar el orden en el que se ejecuta el codigo respecto a otras tareas. Hay que distinguir bien entre qué elementos se ejecutan de manera síncrona, y cuales asíncronos.
La función resolver
que se le pasa al constructor de Promise
se ejecuta de manera síncrona. En cambio, todos los callbacks que se le pasan a then
y a catch
se invocan de manera asíncrona.
Veamoslo con un ejemplo:
var promesa = new Promise(function (resolver, rechazar) {
console.log("Antes de la funcion resolver");
resolver();
});
promesa.then(function() {
console.log("Dentro del callback de completado");
});
console.log("Fin de la cita");
// Consola
// Antes de la funcion resolver
// Fin de la cita
// Dentro del callback de completado
8.2.8. Gestión de errores
Los rechazos y los errores se propagan a través de la cadena de promesas. Cuando se rechaza una promesa, todas las siguientes promesas de la cadena se rechazan como si fuera un dominó. Para detener el efecto dominó, debemos capturar el rechazo.
Para ello, además de utilizar el método then
y pasar como segundo argumento la función para gestionar los errores, el API nos ofrece el método catch
:
catch
ajaxUA('heroes.json').then(function(response) {
console.log("¡Bien!", response);
}).catch(function(error) {
console.error("¡Mal!", error);
});
En la práctica, se utiliza un método catch al final de la cadena para manejar todos los rechazos.
|
Veamos como utilizar un catch
al final de una cadena de promesas:
catch
al final de una cadena de promesasPromise.reject(Error("Algo ha ido mal")).then( (1)
function paso2() { (2)
console.log("Por aquí no pasaré");
}
).then(
function paso3() {
console.log("Y por aquí tampoco");
}
).catch( (3)
function (err) {
console.error("Algo ha fallado por el camino");
console.error(err);
}
);
// Consola
// Algo ha fallado por el camino
// Error: Algo ha ido mal
1 | Creamos una promesa rechazada |
2 | No se ejectuan ni el paso 2, ni el 3, ya que la promesa no se ha cumplido |
3 | En cambio, si lo hace el manejador del error. |
Excepciones y promesas
Una promesa se rechaza si se hace de manera explicita mediante la función rechazar
del constructor, con Promise.reject
o si el callback pasado a then
lanza un error:
Vamos a repetir el ejemplo, pero lanzando un Error
en el constructor de la promesa:
catch
con Error
lanzado por el constructorrechazarCon("¡Malas noticias!").then(
function paso2() {
console.log("Por aquí no pasaré")
}
).catch(
function (err) {
console.error("Y vuelve a fallar");
console.error(err);
}
);
function rechazarCon(cadena) {
return new Promise(function (resolver, rechazar) {
throw Error(cadena);
resolver("No se utiliza");
});
}
// Consola
// Y vuelve a fallar
// Error: ¡Malas noticias!
Este comportamiento resalta la ventaja de realizar todo el trabajo relacionado con la promesa dentro del callback del constructor, para que los errores se capturen automáticamente y se conviertan en rechazos.
Al lanzar el objeto Error para rechazar una promesa, la pila de llamadas se captura, lo que facilita el manejo del error en el manejador catch .
|
Autoevaluación
A partir del siguiente código anterior, ¿Qué mensajes saldrán por pantalla? [2]
|
catch
vs then(undefined, función)
El método catch
es similar a emplear then(undefined, función)
, pero más legible. Por lo tanto, el anterior fragmento (Ejemplo catch
) es equivalente a este:
then(undefined, función)
ajaxUA('heroes.json').then(function(response) {
console.log("¡Bien!", response);
}).then(undefined, function(error) {
console.error("¡Mal!", error);
});
Pero que sean similares no significan que sean iguales. Hay una sutil diferencia entre encadenar dos funciones then
a hacerlo con una sola. Al rechazar una promesa se pasa al siguiente then
con un callback de rechazo (o catch
, ya que son equivalentes). Es decir, con then(func1, func2)
, se llamará a func1 o a func2, nunca a las dos. Pero con
then(func1).catch(func2)
, se llamarán a ambas si func1 rechaza la promesa, ya que son pasos separados de la cadena.
Veamoslo con otro ejemplo:
paso1().then(function() {
return paso2();
}).then(function() {
return paso3();
}).catch(function(err) {
return recuperacion1();
}).then(function() {
return paso4();
}, function(err) {
return recuperacion2();
}).catch(function(err) {
console.error("No me importa nada");
}).then(function() {
console.log("¡Finiquitado!");
});
Este flujo de llamadas es muy similar a emplear try/catch en el lenguaje estandard de manera que los errores que suceden dentro de un try
saltan inmediatamanete al bloque catch
.
8.2.9. Promesas en paralelo
Si tenemos un conjunto de promesas que queremos ejecutar, y lo hacemos mediante un bucle, se ejecutarán en paralelo, en un orden indeterminado finalizando cada una conforme al tiempo necesario por cada tarea.
Supongamos que tenemos una aplicación bancaria en la cual tenemos varias cuentas, de manera que cuando el usuario entra en la aplicación, se le muestra el saldo de cada una de ellas:
var cuentas = ["/banco1/12345678", "/banco2/13572468", "/banco3/87654321"];
cuentas.forEach(function(cuenta) {
ajaxUA(cuenta).then(function(balance) {
console.log(cuenta + " Balance -> " + balance);
});
});
// Consola
// Banco 1 Balance -> 234
// Banco 3 Balance -> 1546
// Banco 2 Balance -> 789
Si además queremos mostrar un mensaje cuando se hayan consultado todas las cuentas, necesitamos consolidar todas las promesas en una nueva. Para ello, mediante el método Promise.all
, podemos escribir código de finalización de tareas paralelas, del tipo "Cuando todas estas cosas hayan finalizado, haz esta otra".
El comportamiento de Promise.all(arrayDePromesas).then(function(arrayDeResultados)
es el siguiente: devuelve una nueva promesa que se cumplirá cuando lo hayan hecho todas las promesas recibidas. Si alguna se rechaza, la nueva promesa también se rechazará. El resultado es un array de resultados que siguen el mismo orden de las promesas recibidas.
Así pues, reescribimos el ejemplo y tendremos:
var cuentas = ["/banco1/12345678", "/banco2/13572468", "/banco3/87654321"];
var peticiones = cuentas.map(function(cuenta) {
return ajaxUA(cuenta);
});
Promise.all(peticiones).then(function (balances) {
console.log("Los " + balances.length + " han sido actualizados");
}).catch(function(err) {
console.error("Error al recuperar los balances", err);
})
// Consola
// Los 3 balances han sido actualizados
8.2.10. Promesas en secuencia
Cuando hemos estudiado como encadenar promesas, hemos visto como podemos codificar que una promesa se ejecute cuando ha finalizado la anterior. Para ello, de antemano tenemos que saber las promesas que vamos a gestionar.
¿Y si el número de promesas es indeterminado? Es decir, si tenemos un array con las promesas que se tienen que ejecutar secuancialmente ¿Cómo puedo hacerlo?
Una solución es hacerlo mediante un bucle iterativo:
reduce
function secuencia(array, callback) {
var seq = Promise.resolve();
array.forEach(function (elem) {
seq = seq.then(function() { (1)
return callback(elem);
});
});
}
secuencia(cuentas, function (cuenta) {
return ajaxUA(cuenta).then(function(balance) {
console.log(cuenta + " Balance -> " + balance);
});
})
1 | en cada iteración, seq contiene el valor de la secuencia anterior hasta que se resuelve y almacena la nueva promesa |
Si empleamos la función reduce
, el código se reduce al ahorrarnos la variable seq
:
reduce
function secuencia(array, callback) {
return array.reduce(function cadena(promesa, elem) { (1)
return promesa.then(function() {
return callback(elem);
});
}, Promise.resolve()); (2)
}
1 | La variable promesa almacena el valor anterior. La función cadena siempre devuelve una promesa que resuelve el callback . |
2 | El array se reduce hasta finalizarlo y devolver la última promesa de la cadena |
En cambio, mediante código recursivo, pese a que pensemos que podemos desbordar la pila, cada llamada se coloca encima de la pila, de manera que el bucle de eventos la resuelve en primer lugar:
function secuencia(array, callback) {
function cadena(array, indice) {
if (indice === array.length) {
return Promise.resolve();
} else {
return Promise.resolve(callback(array[indice])).then(function() {
return cadena(array, indice + 1);
});
}
}
return cadena(array, 0);
}
Aunque ambos planteamientos obtengan el mismo resultado, los dos enfoques tienen sus diferencias. Mientras que mediante el bucle se contruye la cadena entera de promesas sin esperar a que se resuelvan, mediante el planteamiento recursivo, las promesas se añaden bajo demanda tras resolver la promesa anterior. Así pues, el enfoque recursivo permite decidir si continuar o romper la cadena en base al resultado de una promesa anterior.
Las librerías de terceros que permiten gestionar las promesas ofrecen funciones auxiliares para trabajar con promesas en secuencia o de manera paralela. |
8.2.11. Carrera de promesas
En ocasiones vamos a realizar diferentes peticiones que realizan la misma acción a diferentes servicios, pero sólo nos interesará el que nos devuelve el resultado más rápidamente.
Para ello, el API ofrece el método Promise.race(arrayDePromesas)
, la cual reduce el array de promesas y devuelve una nueva promesa con el primer valor disponible. Es decir, se examina cada promesa hasta que una de ellas finaliza, ya sea resuelta o rechazada, la cual se devuelve.
Mediante esta operación, podemos crear un mecanismo para gestionar la latencia de una petición a servidor, de manera que si tarda más de X segundos, acceda a una caché. En el caso de que falle la cache, el mecanismo fallará.
function obtenerDatos(url) {
var tiempo = 500; // ms
var caduca = Date.now() + tiempo;
var datosServer = ajaxUA(url);
var datosCache = buscarEnCache(url).then(function (datos) {
return new Promise(function (resolver, rechazar) {
var lapso = Math.max(caduca: Date.now(), 0);
setTimeout(function () {
resolver(datos);
}, lapso);
})
});
var fallo = new Promise(function (resolver, rechazar) {
setTimeout(function () {
rechazar(new Error("No se ha podido recuperar los datos de " + url));
}, tiempo);
});
return Promse.race([datosServer, datosCache, fallo]);
}
Se trata de una solución simplificada, donde se ha omitido el código de buscarEnCache
, y no se han planteado todos los escenarios posibles.
Autoevaluación
¿Qué pasaría se la petición a los datos del servidor falla inmediatamente debido a un fallo de red? [3] |
Librerías de programación reactiva como RxJS, Bacon.js y Kefir.js están diseñadas especificamente para estos escenarios |
8.3. Fetch API
Aprovechando las promesas, ES6 ofrece el Fetch API para realizar peticiones AJAX que directamente devuelvan un promesa. Es decir, ya no se emplea el objeto XMLHttpRequest
, el cual no se creó con AJAX en mente, sino una serie de métodos y objetos diseñados para tal fin. Al emplear promesas, el código es más sencillo y limpio, evitando los callbacks anidados.
Actualmente, el API lo soportan tanto Google Chrome como Mozilla Firefox. Podéis comprobar el resto de navegadores en http://caniuse.com/#search=fetch |
El objeto window
ofrece el método fetch
, con un primer argumento con la URL de la petición, y un segundo opcional con un objeto literal que permite configurar la petición:
// url (obligatorio), opciones (opcional)
fetch('/ruta/url', {
method: 'get'
}).then(function(respuesta) {
}).catch(function(err) {
// Error :(
});
8.3.1. Hola Fetch
A modo de ejemplo, vamos a reescibir el ejemplo de la sesión 5 que hacíamos uso de AJAX, pero ahora con la Fetch API. De esta manera, el código queda mucho más concreto:
fetch('http://www.omdbapi.com/?s=batman', {
method: 'get'
}).then(function(respuesta) {
if (!respuesta.ok) {
throw Error(respuesta.statusText);
}
return respuesta.json();
}).then(function(datos) {
var pelis = datos.Search;
for (var numPeli in pelis) {
console.log(pelis[numPeli].Title + ": " + pelis[numPeli].Year);
}
}).catch(function(err) {
console.error("Error en Fetch de películas de Batman", err);
});
Como podemos observar, haciendo uso de las promesas, podemos encadenarlas y en el último paso comprobar los errores.
Por ejemplo, podemos refactorizar el control de estado y extraelo a una función:
function estado(respuesta) {
if (respuesta.ok) {
return Promise.resolve(respuesta);
} else {
return Promise.reject(new Error(respuesta.statusText));
}
}
De este modo, nuestro código quedaría así:
fetch('http://www.omdbapi.com/?s=batman', {
method: 'get'
}).then(estado)
.then(function (respuesta) {
return respuesta.json();
}).then(function(datos) {
var pelis = datos.Search;
for (var numPeli in pelis) {
console.log(pelis[numPeli].Title + ": " + pelis[numPeli].Year);
}
}).catch(function(err) {
console.error("Error en Fetch de películas de Batman", err);
});
8.3.2. Cabeceras
Una ventaja de este API es la posibilidad de asignar las cabeceras de las peticiones mediante el objeto Headers()
, el cual tiene una estructura similar a una mapa. A continuación se muestra un ejemplo de como acceder y manipular las cabeceras mediante los métodos append
, has
, get
, set
y delete
:
var headers = new Headers();
headers.append('Content-Type', 'text/plain');
headers.append('Mi-Cabecera-Personalizada', 'cualquierValor');
headers.has('Content-Type'); // true
headers.get('Content-Type'); // "text/plain"
headers.set('Content-Type', 'application/json');
headers.delete('Mi-Cabecera-Personalizada');
// Add initial values
var headers = new Headers({ (1)
'Content-Type': 'text/plain',
'Mi-Cabecera-Personalizada': 'cualquierValor'
});
Para usar las cabeceras, las pasaremos como parámetro de creación de una petición mediante una instancia de Request
:
var peticion = new Request('/url-peticion', {
headers: new Headers({
'Content-Type': 'text/plain'
})
});
fetch(peticion).then(function() { /* manejar la respuesta */ });
8.3.3. Petición
Para poder configurar toda la información que representa una petición se emplea el objeto Request
. Este objeto puede contener las siguientes propiedades:
-
method
:GET
,POST
,PUT
,DELETE
,HEAD
-
url
: URL de la petición -
headers
: objeto Headers con las cabeceras asociadas -
body
: datos a enviar con la petición -
referrer
: referrer de la petición -
mode
:cors
,no-cors
,same-origin
-
credentials
: indica si se envian cookies con la petición:include
,omit
,same-origin
-
redirect
:follow
,error
,manual
-
integrity
: valor integridad del subrecurso -
cache
: tipo de cache (default
,reload
,no-cache
)
Con estos parámetros, una petición puede quedar así:
var request = new Request('/heroes.json', {
method: 'get',
mode: 'cors',
headers: new Headers({
'Content-Type': 'text/plain'
})
});
fetch(request).then(function() { /* manejar la respuesta */ });
Realmente, el método fetch
recibe una URL a la que se envía una petición, y un objeto literal con la configuración de la petición, por lo que el código queda mejor así:
fetch('/heroes.json', {
method: 'GET',
mode: 'cors',
headers: new Headers({
'Content-Type': 'text/plain'
})
}).then(function() { /* manejar la respuesta */ });
Enviando datos
Ya sabemos que un caso muy común es enviar la información de un formulario vía AJAX.
Para ello, además de configurar que la petición sea POST
, le asociaremos en la propiedad body
un FormData
creado a partir del identificador del elemento del formulario:
fetch('/submit', {
method: 'post',
body: new FormData(document.getElementById('formulario-cliente'))
});
Si lo que queremos es enviar JSON al servidor, en la misma propiedad, le asociamos un nuevo objeto JSON:
fetch('/submit-json', {
method: 'post',
body: JSON.stringify({
email: document.getElementById('email').value
comentarios: document.getElementById('comentarios').value
})
});
O si queremos enviar información en formato URL:
fetch('/submit-urlencoded', {
method: 'post',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
body: 'heroe=Batman&nombre=Bruce+Wayne'
});
8.3.4. Respuesta
Una vez realizada la petición, dentro del manejador, reciberemos un objeto Response
, compuesto de una serie de propiedades que representan la respuesta del servidor, de las cuales extraremos la información:
-
type
: indican el origen de la petición. Dependiendo del tipo, podremos consultar diferente información:-
basic
: proviene del mismo origen, sin restricciones. -
cors
: acceso permitido a origen externo. Las cabeceras que se pueden consultar sonCache-Control
,Content-Language
,Content-Type
,Expires
,Last-Modified
, yPragma
-
opaque
: origen externo que no devuelve cabeceras CORS, con lo que no podemos leer los datos ni visualizar el estado de la petición.
-
-
status
: código de estado (200, 404, etc.) -
ok
: Booleano que indica si la respuesta fue exitosa (en el rango 200-299 de estado) -
statusText
: código de estado (OK
) -
headers
: objeto Headers asociado a la respuesta.
Además, el objeto Response
ofrece los siguientes métodos para crear nuevas respuestas con diferente código de estado o a un destino distino:
-
clone()
: clona el objeto Response -
error()
: devuelve un nuevo objeto Response asociado con un error de red. -
redirect()
: crea una nueva respuesta con una URL diferente
Finalmente, para transformar la respuesta a una promesa con un tipo de dato del cual extraer la información de la petición, tenemos los siguientes métodos:
-
arrayBuffer()
: Devuelve una promesa que se resuelve con un ArrayBuffer. -
blob()
: Devuelve una promesa que se resuelve con un Blob. -
formData()
: Devuelve una promesa que se resuelve con un objeto FormData. -
json()
: Devuelve una promesa que se resuelve con un objeto JSON. -
text()
: Devuelve una promesa que se resuelve con un texto (USVString).
Veamos estos métodos en funcionamiento.
Parseando la respuesta
A día de hoy, el estandar es emplear el formato JSON para las respuestas. En vez de utilizar JSON.parse(cadena)
para transformar la cadena de respuesta en un objeto, podemos utilizar el método json()
:
fetch('heroes.json').then(function(response) {
return response.json();
}).then(function(datos) {
console.log(datos); // datos es un objeto JavaScript
});
Si la información, viene en texto plano o como documento HTML, podemos emplear text()
:
fetch('/siguientePagina').then(function(response) {
return response.text();
}).then(function(texto) {
// <!DOCTYPE ....
console.log(texto);
});
Finalmente, si recibimos una imagen o archivo en binario, hemos de emplear blob()
:
fetch('paisaje.jpg').then(function(response) {
return response.blob();
})
.then(function(blobImagen) {
document.querySelector('img').src = URL.createObjectURL(blobImagen);
});
Más ejemplos en https://blog.gospodarets.com/fetch_in_action/
8.4. jQuery Deferreds
Los Deferreds son la implementación que hace jQuery de las promesas, ofreciendo una implementación de las promesas independiente de la versión de ECMAScript que tenga nuestro navegador.
8.4.1. $.Deferred()
Cada promesa de jQuery comienza con un Deferred
.
Un Deferred
es solo una promesa con métodos que permiten a su propietario resolverla o rechazarla. Todas las otras promesas son de sólo lectura.
Para crear un Deferred
, usaremos el constructor $.Deferred()
. Una vez creada una promesa podemos invocar los siguientes métodos:
-
state()
: devuelve el estado -
resolve()
: resuelve la promesa -
reject()
: rechaza la promesa
var deferred = new $.Deferred();
deferred.state(); (1)
deferred.resolve(); (2)
deferred.state(); (3)
deferred.reject(); (4)
1 | Devuelve el estado de la promesa. Nada más creada su estado es pending . |
2 | Le indicamos a la promesa que ha finalizado la acción que queremos controlar. |
3 | Volvemos a obtener el estado, pero ahora ya es resolved . |
4 | Permite rechazar la promesa. En este ejemplo no tiene efecto, ya que la promesa se ha resuelto. |
Mucho cuidado con diseñar una aplicación que se base en comprobar el estado de una promesa para actuar en consecuencia. El único momento que tiene sentido actuar dependiendo de su valor es cuando tras un callback de done() querramos averiguar si hemos llegado tras resolver o rechazar la promesa.
|
Si al método constructor le pasamos una función, ésta se ejecutará tan pronto como el objete se cree, y la función recibe como parámetro el nuevo objeto deferred. Mediante esta característica podemos crear un envoltorio que realiza una tarea asíncrona y que dispare un callback cuando haya finalizado:
function realizarTarea() {
return $.Deferred(function(def) {
// tarea async que dispara un callback al acabar
});
}
Podemos obtener una promesa "pura" a partir del método promise()
. El resultado es similar a Deferred
, excepto que faltan los métodos de resolve()
y reject()
.
var deferred = new $.Deferred();
var promise = deferred.promise();
promise.state(); // "pending"
deferred.reject();
promise.state(); // "rejected"
El método promise()
existe principalmente para dar soporte a la encapsulación: si una función devuelve un Deferred
, puede ser resuelta o rechazada por el programa que la invoca. En cambio, si sólo devolvemos la promesa pura correspondiente al Deferred
, el programa que la invoca sólo puede leer su estado y añadir callbacks. La propia jQuery sigue este enfoque al devolver promesas puras desde sus métodos AJAX:
var obteniendoProductos = $.get("/products");
obteniendoProductos.state(); // "pending"
obteniendoProductos.resolve; // undefined
Las promesas se suelen nombrar con gerundios para indicar que representan un proceso |
8.4.2. Manejadores de promesas
Una vez tenemos una promesa, podemos adjuntarle tantos callbacks como queremos mediante los métodos:
-
done()
: se lanza cuando la promesa se resuelve. correctamente, medianteresolve()
∫ -
fail()
: se lanza cuando la promesa se rechaza, mediantereject()
-
always()
: se lanza cuando se completa la promesa, independientemente que su estado sea resuelta o rechazada
Por ejemplo:
var promesa = $.Deferred();
promesa.done(function() {
console.log("Se ejecutará cuando la promesa se resuelva.");
});
promesa.fail(function() {
console.log("Se ejecutará cuando la promesa se rechace.");
});
promesa.always(function() {
console.log("Se ejecutará en cualquier caso.");
});
Por ejemplo, supongamos que tenemos tres botones que modifican la promesa:
<button id="btnResuelve">Resolver</button>
<button id="btnRechaza">Rechazar</button>
<button id="btnEstado">¿Estado?</button>
Y si capturamos los eventos click para que modifiquen la promesa, tendremos:
$("#btnResuelve").click(function() {
promesa.resolve();
});
$("#btnRechaza").click(function() {
promesa.reject();
});
$("#btnEstado").click(function() {
console.log(promesa.state());
});
8.4.3. Encadenando promesas
Los métodos de los objetos Deferred
se pueden encadenar, de manera que cada método devuelve a su vez un objeto Deferred
donde poder volver a llamar a otros métodos:
promesa.done(function() {
console.log("Se ejecutará cuando la promesa se resuelva.");
}).fail(function() {
console.log("Se ejecutará cuando la promesa se rechace.");
}).always(function() {
console.log("Se ejecutará en cualquier caso.");
});
Mediante el método then()
podemos agrupar los tres callbacks de una sola vez:
promesa.then(doneCallback, failCallback, alwaysCallback);
Con lo que si reescribimos el ejemplo tendríamos:
promesa.then(function() {
console.log("Se ejecutará cuando la promesa se resuelva.");
}, function() {
console.log("Se ejecutará cuando la promesa se rechace.");
}, function() {
console.log("Se ejecutará en cualquier caso.");
});
Además, el orden en el que se adjuntan los callbacks definen su orden de ejecución. Por ejemplo, si duplicamos los callbacks y volvemos a añadirlos, tendremos:
Supongamos que reescribimos el ejemplo anterior con el siguiente fragmento para manejar las promesas:
var promesa = $.Deferred();
promesa.done(function() {
console.log("Primer callback.");
}).done(function() {
console.log("Segundo callback.");
}).done(function() {
console.log("Tercer callback.");
});
promesa.fail(function() {
console.log("Houston! Tenemos un problema");
});
promesa.always(function() {
console.log("Dentro del always");
}).done(function() {
console.log("Y un cuarto callback si todo ha ido bien");
});
Si probamos a resolver la promesa, la salida por consola será la siguiente:
"Primer callback."
"Segundo callback."
"Tercer callback."
"Dentro del always"
"Y un cuarto callback si todo ha ido bien"
8.4.4. Modelando con promesas
Veamos en detalle un caso de uso de promesas para representar una serie de acciones que puede realizar el usuario a partir de un formulario básico con AJAX que contiene un campo de texto con observaciones.
Queremos asegurarnos que el formularios solo se envia una vez y que el usuario recibe una confirmación cuando envía las observaciones. Además, queremos separar el código que describe el comportamiento de la aplicación del código que trabaja el contenido de la página. Esto facilita el testing y minimiza la cantidad de código necesario que necesitaríamos modificar si cambiamo la disposición de la página.
Así pues, separamos la lógica de aplicación mediante promesas:
var enviandoObservaciones = new $.Deferred();
enviandoObservaciones.done(function(input) {
$.post("/observaciones", input);
});
Y la interacción DOM se encarga de modificar el estado de la promesa:
$("#observaciones").submit(function() {
enviandoObservaciones.resolve($("textarea", this).val()); (1)
return false; (2)
});
enviandoObservaciones.done(function() {
$("#contenido").append("<p>¡Gracias por las observaciones!</p>"); (3)
});
1 | Cuando el usuario pulsa sobre enviar, se resuelve la promesa. Si le pasamos un parámetro a un método de promesa (ya sea ), el parámetro se reenvía al callback correspondiente. |
2 | Prevenimos el comportamiento por defecto del navegador. |
3 | Le añadimos un segundo manejador que nos permite separar el código de aplicación del tratamiento del DOM. |
8.4.5. Prestando promesas del futuro
Si queremos mejorar nuestro formulario, más que asumir de manera optimista que nuestro POST funcionará correctamente, deberíamos primero indicar que el formulario se ha enviado, por ejemplo, mediante una barra de progreso, y posteriormente indicarle al usuario si el envío ha sido exitoso o fallido cuando el servidor responda.
Para ello, podemos adjuntar callbacks a la promesa que devuelve $.post
. El problema viene cuando tenemos que manipular el DOM desde esos callbacks, y hemos planteado usar promesas para separar el código de aplicación del de manipulación del DOM.
Una manera de separar la creación de una promesa del callback de lógica de aplicación es reenviar los eventos de resolve
/reject
desde la promesa POST a una promesa que se encuentre fuera de nuestro alcance. En vez de necesitar varias líneas con código anidado del tipo promesa1.done(promesa2.resolve);..
, lo haremos mediante then()
(antes de jQuery 1.8 mediante pipe()
).
Para ello, then()
devuelve una nueva promesa que permite filtrar el estado y los valores de una promesa mediante una función, lo que la convierte en una ventana al futuro, permitiendo adjuntar comportamiento a una promesa que todavía no existe.
Si ahora mejoramos el código del formulario haciendo que nuestra promesa POST se encole en una nueva promesa llamada guardandoObservaciones
, tendremos que el código de aplicación quede así:
var enviandoObservaciones = new $.Deferred();
var guardandoObservaciones = enviandoObservaciones.pipe(function(input) {
return $.post("/observaciones", input); (1)
});
1 | Encolamos la promesa de guardado tras el envío |
Con lo que la manipulación del DOM queda del siguiente modo:
$("#observaciones").submit(function() {
enviandoObservaciones.resolve($("textarea", this).val());
return false;
});
enviandoObservaciones.done(function() {
$("#contenido").append("<div class='spinner'>"); (1)
});
guardandoObservaciones.then(
function() { // done
$("#contenido").append("<p>¡Gracias por las observaciones!</p>");
}, function() { // fail
$("#contenido").append("<p>Se ha producido un error al contactar con el servidor.</p>");
}, function() { // always
$("#contenido").remove(".spinner"); (2)
});
1 | Al enviar los datos creamos la barra de progreso |
2 | Cuando finaliza el guardado, quitamos la barra de progreso |
Intersección de promesas
Parte de la magia de las promesas es su naturaleza binaria. Como solo tienen dos estados, se pueden combinar igual que si fuesen booleanos (aunque todavía no sepamos que valores tendrán).
De la misma manera que tenemos Promise.all
, jQuery ofrece el método $.when()
como operador equivalente a la intersección (Y).
Así pues, dada una lista de promesas, when()
devuelve una nueva promesa como resultado de otras promesas y que cumple estas reglas:
-
Cuando todas las promesas recibidas se resuelven, la nueva promesa estará resuelta.
-
Cuando alguna de las promesas recibidas se rechaza, la nueva promesa se rechaza.
Por ello, cuando estemos esperando que múltiples eventos desordenados ocurran y necesitamos realizar una acción cuando todos hayan finalizado deberemos utilizar when()
. Por ejemplo, el caso de uso más común es al realizar varias llamadas AJAX simultáneas:
$("#contenido").append("<div class='spinner'>");
$.when( $.get("/datosEncriptados"),$.get("/claveEncriptacion"))
.then(function() { // done
// ambas llamadas han funcionado
}, function() { // fail
// una de las llamadas ha fallado
}, function() { // always
$("#contenido").remove(".spinner");
});
Si las dos promesas se resuelven, entonces la promesa agregada también se resuelve, y se llamará la función del primer callback (done()
). Sin embargo, en cuanto una de las promesas se rechace, la promesa agregada se rechazará también, disparando el callback de fail()
.
Otro caso de uso es permitir al usuario solicitar un recurso que puede o no estar disponible. Por ejemplo, supongamos un widget de un chat que cargamos con YepNope:
var cargandoChat = new $.Deferred();
yepnope({
load: "recursos/chat.js",
complete: cargandoChat.resolve
});
var lanzandoChat = new $.Deferred();
$("#lanzarChat").click(lanzandoChat.resolve);
lanzandoChat.done(function() {
$("#contenidoChat").append("<div class='spinner'>");
});
$.when(cargandoChat, lanzandoChat).done(function() {
$("#contenidoChat").remove(".spinner");
// comienza el chat
});
8.4.6. Deferreds y AJAX
Las promesas facilitan mucho el trabajo con AJAX y el trabajo con acceso simultáneos a recursos remotos.
El objeto jxXHR
que se obtiene de los métodos AJAX como $.ajax()
o $.getJSON()
implementan el interfaz Promise
, por lo que vamos a poder utilizar los métodos done
, fail
, then
, always
y when
.
Así pues, vamos a poder escribir código similar al siguiente, donde tras hacer la petición vamos a poder utilizar los callbacks que ofrecen las promesas:
Ejemplo AJAX y Deferreds: http://jsbin.com/ponaca/edit?html,js,console,output
function getDatos() {
var peticion = $.getJSON("http://www.omdbapi.com/?s=batman&callback=?");
peticion.done(todoOk).fail(function() {
console.log("Algo ha fallado");
});
peticion.always(function() {
console.log("Final, bien o mal");
});
}
function todoOk(datos) {
console.log("Datos recibidos y adjuntándolos a resultado");
$("#resultado").append(JSON.stringify(datos));
}
En resumen, el uso de promesas simplifica la gestión de eventos asíncronos evitando múltiples condiciones anidadas. Ya veremos en el módulo de Angular como dicho framework hace un uso extenso de las promesas.
8.5. Ejercicios
8.5.1. (0.5 ptos) Ejercicio 81. Star Wars Fetch
A partir del ejercicio 72 sobre Star Wars API, vamos a reescribir el contenido mediante Fetch API y el uso de promesas para reducir el código y simplificar la lógica de las llamadas.
En este caso, el listado de películas se tiene que pintar en el DOM de una sola vez, no conforme se reciben los títulos de las películas con cada petición AJAX.
Todo el código estará incluido en el archivo ej81.js
.
8.5.2. (0.3 ptos) Ejercicio 82. Star Wars Deferreds
A partir del ejercicio 72 sobre Star Wars API, se pide reescribir el contenido para hacer uso de Deferreds.
De igual manera que el ejercicio anterior, el listado de películas se tiene que pintar en el DOM de una sola vez, no conforme se reciben los títulos de las películas con cada petición AJAX.
Todo el código estará incluido en el archivo ej82.js
.