Como vimos en el posteo anterior, Javascript cuenta con diferentes mecanismos para manejar el asincronismo. Uno de los tantos, son las Promises (promesas) y son éstas las que veremos en este articulo.
Una promesa simboliza un valor que puede estar disponible ahora, en el futuro o nunca, es decir que representa la finalización exitosa o fracaso eventual de una determinada operación asincrónica.
Pero, por que deberíamos usarlas?
Supongamos el siguiente ejemplo.
Queremos hacer peticiones Ajax dependientes entre si, la primera de ellas es para obtener los datos de usuarios que tenemos en la aplicación
const xhr = new XMLHttpRequest();
xhr.open("get", "https://jsonplaceholder.typicode.com/users");
xhr.addEventListener("load", function(){
let usuarios = JSON.parse(xhr.response);
console.log(usuarios);
});
xhr.send();
const xhr = new XMLHttpRequest()
xhr.open("get", "https://jsonplaceholder.typicode.com/users")
xhr.addEventListener("load", function(){
let usuarios = JSON.parse(xhr.response)
console.log(usuarios);
var xhr_posts = new XMLHttpRequest()
xhr_posts.open("get" , "https://jsonplaceholder.typicode.com/posts?userId=" + usuarios[0].id)
xhr_posts.addEventListener("load", function(){
var posts = JSON.parse(xhr_posts.response)
console.log(posts);
})
xhr_posts.send()
})
xhr.send();
const xhr = new XMLHttpRequest()
xhr.open("get", "https://jsonplaceholder.typicode.com/users")
xhr.addEventListener("load", function(){
let usuarios = JSON.parse(xhr.response)
console.log(usuarios);
var xhr_posts = new XMLHttpRequest()
xhr_posts.open("get" , "https://jsonplaceholder.typicode.com/posts?userId=" + usuarios[0].id)
xhr_posts.addEventListener("load", function(){
var posts = JSON.parse(xhr_posts.response)
console.log(posts);
var xhr_comentarios = new XMLHttpRequest();
xhr_comentarios.open("GET", "https://jsonplaceholder.typicode.com/comments?postId=" + posts[ 0 ].id);
xhr_comentarios.addEventListener("load", function(){
var comentarios = JSON.parse(xhr_comentarios.response);
console.log(comentarios);
})
xhr_comentarios.send();
})
xhr_posts.send()
})
xhr.send();
Podemos ver que se genera un triangulo con el formato de nuestro código debido a la dependencia de código asincrónico, ya que una petición depende de la respuesta que se obtiene de una petición anterior y nos puede llegar a confundir para saber en donde estamos ubicados dentro de nuestro programa.
A esto se lo conoce como "Pyramid of doom" o "Callback hell", y lo podemos resolver mediante Promesas.
PROMESAS
Las promesas son objetos que nos permiten escribir código asincrónico de una manera encadenable, es decir, que en lugar de estar pasando los callbacks dentro de las funciones, podemos ir encadenadolos de tal forma que nuestro código se pueda leer como si estuviese escrito secuencialmente.
Veamos un ejemplo para obtener la lista de usuarios con una promesa
const promesaUsuarios = new Promise(function(resolve, reject){
const xhr = new XMLHttpRequest();
xhr.open("get", "https://jsonplaceholder.typicode.com/users");
xhr.addEventListener("load", function(){
if(xhr.status == 200){
let usuarios = JSON.parse(xhr.response);
resolve(usuarios);
}else{
reject("No se encontraron usuarios");
}
});
xhr.send();
});
Cuando se crea la Promise, se la pasa un ejecutor, que es una función que generalmente hará un trabajo asincrónico, y que luego una vez completado ejecuta uno de los callbacks que se le pasaron, en el ejemplo fueron nombrados como resolve y reject pero sus nombre puede no ser necesariamente esos.
Estos callbacks al ser llamados, pueden resolver o rechazar la promesa respectivamente.
El objeto Promise tiene un estado con los posibles valores:
- Pending: es el valor inicial, aun la promesa no fue resuelta
- Fulfilled: ocurre cuando la promesa se cumple
- Rejected: ocurre cuando la promesa no se cumple
Cuando una promesa se completa o se rechaza, podemos llamar a sus métodos .then (en caso de que se complete) o .catch (en caso de que se rechace),
estos métodos devuelven una Promesa, y nos permite aprovechar un gran beneficio que tienen, que es ser encadenadas.
Principio de encadenamiento
El encadenamiento nos permite ejecutar dos o mas operaciones asincrónicas seguidas, donde cada operación se inicia con el resultado exitoso de la operación anterior.
promesaUsuarios
.then(valor => {
return new Promise(function(resolve, reject){
const xhr_posts = new XMLHttpRequest();
xhr_posts.open("get", "https://jsonplaceholder.typicode.com/posts?userId=" + valor[0].id);
xhr_posts.addEventListener("load", function(){
if(xhr_posts.status == 200){
let posts = JSON.parse(xhr_posts.response);
resolve(posts);
}else{
reject("No se encontraron posteos");
}
});
xhr_posts.send();
})
})
.then(valor => {
return new Promise(function(resolve, reject){
const xhr_comentarios = new XMLHttpRequest();
xhr_comentarios.open("get", "https://jsonplaceholder.typicode.com/comments?postId=" + valor[0].id);
xhr_comentarios.addEventListener("load", function(){
if(xhr_comentarios.status == 200){
let comentarios = JSON.parse(xhr_comentarios.response);
resolve(comentarios);
}else{
reject("No se encontraron comentarios");
}
});
xhr_comentarios.send();
})
})
.catch(error => {
console.error(error);
});
En el ejemplo podemos ver como mediante consecutivos .then, encadenamos todas las llamadas al API sin necesidad de callbacks y retornando otra Promise que será el punto de entrada para la siguiente llamada al API. De ésta manera rompemos esa forma de pirámide en nuestro código y podemos tenerlo de una manera mas secuencial resultando así un código mas legible y mantenible que los callbacks.
Además, otra ventaja de las promesas, es que Javascript le va a dar una mayor prioridad a las microtareas en el Promise Queue antes que las tareas del Callback Queue (para saber como funciona Javascript te recomiendo leer el siguiente articulo).
FECTH API
La Web API Fetch nos proporciona una interfaz para interactuar con recursos de una manera mas flexible y potente que XMLHttpRequest, ya que trabaja con Promesas en lugar de callbacks y además nos da una definición genérica de los objetos Request y Response.
Veamos el siguiente ejemplo para realizar una petición GET con fetch, tan solo con pasar por por parámetro la URL del recurso a la función fetch, ya podemos realizar la petición y nos devolverá una promesa que se resuelve en un objeto Response a esa petición, ya sea que finalice de manera exitosa o no.
En este caso, una vez que realizamos la petición, vamos a llamar a la función handleJsonRequest que va a ser la encargada de transformarnos la respuesta obtenida al formato json, y luego poder concatenar los .then() que creamos necesarios para realizar nuestra operación.
function pedirUsuarios(){
fetch("https://jsonplaceholder.typicode.com/users")
.then(handleJsonRequest)
.then(usuarios => console.log(usuarios))
.catch(error => console.log(error));
}
function handleJsonRequest(response){
if(!response.ok){
throw new Error("Error de ajax");
}
return response.json();
}
El objeto response representa la respuesta a nuestra petición y cuenta con varias propiedades y métodos para transformar el cuerpo de la respuesta
headers | Contiene un objeto con Headers asociado |
ok | Contiene un booleano correspondiente al estado final de la solicitud |
status | Contiene el codigo HTTP asociado a la respuesta |
json() | Transforma el body de la respuesta en formato JSON y retorna una promesa |
blob() | Transforma el body de la respuesta en formato Blob y retorna una promesa |
Ademas de una peticion GET, podemos realizar una peticion POST agregando un parametro adicional a fetch(), que es el objeto Request
function agregarUsuario(usuario){
fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(usuario)
})
.then(handleJsonRequest)
.then(json => console.log(json))
.catch(error => console.log(error));
}
En el ejemplo vemos como además de la URL del recurso, agregamos un objeto que contiene un method, headers y un body de la peticion y al igual que
el objeto Responde, Request cuenta con propiedades configurables para realizar el pedido.
method | Contiene el metodo de la solicitud |
url | Contiene la URL de la solicitud |
headers | Contiene un objeto Headers asociado |
body | Contiene un objeto Body asociado |