Encadenamiento de Promesas
Introducción
En el capítulo anterior sobre callbacks, hablamos de cómo manejar una secuencia de tareas asincrónicas que deben ejecutarse una tras otra, como la carga de scripts. Ahora veremos cómo hacer esto de manera correcta utilizando promesas. En este capítulo, cubriremos el encadenamiento de promesas.
Ejemplo Básico
Considera el siguiente ejemplo de encadenamiento de promesas:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
Aquí, el resultado pasa a través de una cadena de manejadores .then
.
- La promesa inicial se resuelve después de 1 segundo (*).
- El primer manejador
.then
(**), recibe el resultado, muestra una alerta con 1 y retornaresult * 2
. - El siguiente
.then
(***) recibe el resultado anterior, muestra una alerta con 2, y retornaresult * 2
. - El último
.then
recibe el resultado, muestra una alerta con 4 y retornaresult * 2
.
Cada llamada a .then
devuelve una nueva promesa, lo que permite que el siguiente .then
se encadene y reciba el resultado del anterior.
Un Error Común
Es posible agregar múltiples .then
a una sola promesa, pero esto no es encadenamiento. Aquí hay un ejemplo de este error:
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
En este caso, todos los manejadores .then
reciben el mismo resultado y se ejecutan independientemente, mostrando siempre 1. El encadenamiento es mucho más útil y común en la práctica.
Devolviendo Promesas
Un manejador .then
puede devolver una promesa, lo que permite encadenar acciones asincrónicas.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
En este código, cada .then
devuelve una nueva promesa, que se resuelve después de un segundo, con el resultado multiplicado por 2. La salida es la misma que antes: 1 → 2 → 4, pero con un retraso de 1 segundo entre las alertas.
Ejemplo: Carga de Scripts
Veamos cómo encadenar promesas para cargar scripts uno por uno:
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
one();
two();
three();
});
Este código puede simplificarse usando funciones de flecha:
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
one();
two();
three();
});
Cada llamada a loadScript
devuelve una promesa, y el siguiente .then
se ejecuta cuando la promesa se resuelve.
Objetos Thenables
Un manejador .then
puede devolver un objeto llamado “thenable”, que es un objeto con un método .then
.
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { código nativo }
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // muestra 2 después de 1000 ms
JavaScript reconoce los objetos thenables y los trata como promesas, permitiendo integrarlos en cadenas de promesas.
Ejemplo Completo: Fetch
En programación frontend, las promesas son útiles para solicitudes de red. Veamos un ejemplo utilizando fetch
para cargar información de usuario desde un servidor:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
Para hacer que la cadena sea extensible, devolvemos una promesa que se resuelve cuando el avatar termina de mostrarse:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
.then(githubUser => alert(`Terminado de mostrar ${githubUser.name}`));
Finalmente, podemos dividir el código en funciones reutilizables:
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
Resumen
Si un manejador .then
(o catch
/finally
) devuelve una promesa, el resto de la cadena esperará hasta que esta se resuelva o rechace. El resultado o error pasará al siguiente manejador en la cadena.