Volvamos al problema mencionado en el capítulo Introducción: devoluciones de llamada: tenemos una secuencia de tareas asincrónicas que se deben realizar una tras otra, por ejemplo, cargar scripts. ¿Cómo podemos codificarlo bien?
Las promesas proporcionan un par de recetas para lograrlo.
En este capítulo cubrimos el encadenamiento de promesas.
Se parece a esto:
nueva Promesa(función(resolver, rechazar) { setTimeout(() => resolver(1), 1000); // (*) }).entonces(función(resultado) { // (**) alerta(resultado); // 1 resultado de retorno * 2; }).entonces(función(resultado) { // (***) alerta(resultado); // 2 resultado de retorno * 2; }).entonces(función(resultado) { alerta(resultado); // 4 resultado de retorno * 2; });
La idea es que el resultado pase a través de la cadena de controladores .then
.
Aquí el flujo es:
La promesa inicial se resuelve en 1 segundo (*)
,
Luego se llama al controlador .then
(**)
, que a su vez crea una nueva promesa (resuelta con 2
valores).
El siguiente then
(***)
obtiene el resultado del anterior, lo procesa (duplica) y lo pasa al siguiente controlador.
…etcétera.
A medida que el resultado pasa a lo largo de la cadena de controladores, podemos ver una secuencia de llamadas alert
: 1
→ 2
→ 4
.
Todo funciona, porque cada llamada a .then
devuelve una nueva promesa, de modo que podemos llamar al siguiente .then
.
Cuando un controlador devuelve un valor, se convierte en el resultado de esa promesa, por lo que se llama al siguiente .then
con él.
Un error clásico de novato: técnicamente también podemos agregar muchos .then
a una sola promesa. Esto no es encadenar.
Por ejemplo:
let promesa = nueva promesa (función (resolver, rechazar) { setTimeout(() => resolver(1), 1000); }); promesa.entonces(función(resultado) { alerta(resultado); // 1 resultado de retorno * 2; }); promesa.entonces(función(resultado) { alerta(resultado); // 1 resultado de retorno * 2; }); promesa.entonces(función(resultado) { alerta(resultado); // 1 resultado de retorno * 2; });
Lo que hicimos aquí es simplemente agregar varios controladores a una promesa. No se pasan el resultado entre ellos; en cambio, lo procesan de forma independiente.
Aquí está la imagen (compárala con el encadenamiento de arriba):
.then
todos con la misma promesa obtienen el mismo resultado: el resultado de esa promesa. Entonces, en el código anterior, todas alert
muestran lo mismo: 1
.
En la práctica, rara vez necesitamos varios controladores para una promesa. El encadenamiento se utiliza con mucha más frecuencia.
Un controlador, utilizado en .then(handler)
puede crear y devolver una promesa.
En ese caso, los demás controladores esperan hasta que se asiente y luego obtienen su resultado.
Por ejemplo:
nueva Promesa(función(resolver, rechazar) { setTimeout(() => resolver(1), 1000); }).entonces(función(resultado) { alerta(resultado); // 1 devolver nueva Promesa((resolver, rechazar) => { // (*) setTimeout(() => resolver(resultado * 2), 1000); }); }).entonces(función(resultado) { // (**) alerta(resultado); // 2 devolver nueva Promesa((resolver, rechazar) => { setTimeout(() => resolver(resultado * 2), 1000); }); }).entonces(función(resultado) { alerta(resultado); // 4 });
Aquí el primer .then
muestra 1
y devuelve new Promise(…)
en la línea (*)
. Después de un segundo, se resuelve y el resultado (el argumento de resolve
, aquí es result * 2
) se pasa al controlador del segundo .then
. Ese controlador está en la línea (**)
, muestra 2
y hace lo mismo.
Entonces, el resultado es el mismo que en el ejemplo anterior: 1 → 2 → 4, pero ahora con un retraso de 1 segundo entre llamadas alert
.
Devolver promesas nos permite construir cadenas de acciones asincrónicas.
Usemos esta característica con el loadScript
prometido, definido en el capítulo anterior, para cargar scripts uno por uno, en secuencia:
loadScript("https://javascript.info/article/promise-chaining/one.js") .entonces(función(guión) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .entonces(función(guión) { return loadScript("https://javascript.info/article/promise-chaining/tres.js"); }) .entonces(función(guión) { // usar funciones declaradas en scripts // para mostrar que efectivamente cargaron uno(); dos(); tres(); });
Este código se puede acortar un poco con funciones de flecha:
loadScript("https://javascript.info/article/promise-chaining/one.js") .entonces(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .entonces(script => loadScript("https://javascript.info/article/promise-chaining/tres.js")) .entonces(guion => { // los scripts están cargados, podemos usar las funciones declaradas allí uno(); dos(); tres(); });
Aquí, cada llamada loadScript
devuelve una promesa y la .then
se ejecuta cuando se resuelve. Luego inicia la carga del siguiente script. Entonces los scripts se cargan uno tras otro.
Podemos agregar más acciones asincrónicas a la cadena. Tenga en cuenta que el código sigue siendo "plano": crece hacia abajo, no hacia la derecha. No hay señales de la “pirámide de la fatalidad”.
Técnicamente, podríamos agregar .then
directamente a cada loadScript
, así:
loadScript("https://javascript.info/article/promise-chaining/one.js").entonces(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").luego(script2 => { loadScript("https://javascript.info/article/promise-chaining/tres.js").entonces(script3 => { // esta función tiene acceso a las variables script1, script2 y script3 uno(); dos(); tres(); }); }); });
Este código hace lo mismo: carga 3 scripts en secuencia. Pero “crece hacia la derecha”. Entonces tenemos el mismo problema que con las devoluciones de llamada.
Las personas que empiezan a utilizar promesas a veces no saben acerca del encadenamiento, por lo que lo escriben de esta manera. Generalmente se prefiere el encadenamiento.
A veces está bien escribir .then
directamente, porque la función anidada tiene acceso al alcance externo. En el ejemplo anterior, la devolución de llamada más anidada tiene acceso a todas las variables script1
, script2
, script3
. Pero eso es más una excepción que una regla.
Entoncesables
Para ser precisos, un controlador puede devolver no exactamente una promesa, sino un objeto llamado "entonces", un objeto arbitrario que tiene un método .then
. Se tratará del mismo modo que una promesa.
La idea es que las bibliotecas de terceros puedan implementar sus propios objetos "compatibles con la promesa". Pueden tener un conjunto extendido de métodos, pero también ser compatibles con promesas nativas, porque implementan .then
.
A continuación se muestra un ejemplo de un objeto que se puede realizar:
clase Entoncesable { constructor(núm) { this.núm = núm; } entonces(resolver, rechazar) { alerta(resolver); // función() { código nativo } // resolver con this.num*2 después de 1 segundo setTimeout(() => resolver(this.num * 2), 1000); // (**) } } nueva Promesa(resolver => resolver(1)) .entonces(resultado => { devolver nuevo Thenable(resultado); // (*) }) .entonces(alerta); // muestra 2 después de 1000ms
JavaScript verifica el objeto devuelto por el controlador .then
en la línea (*)
: si tiene un método invocable llamado then
, entonces llama a ese método proporcionando funciones nativas resolve
, reject
como argumentos (similar a un ejecutor) y espera hasta que uno de ellos se llama. En el ejemplo anterior, se llama resolve(2)
después de 1 segundo (**)
. Luego, el resultado se transmite más abajo en la cadena.
Esta característica nos permite integrar objetos personalizados con cadenas de promesa sin tener que heredar de Promise
.
En la programación frontend, las promesas se utilizan a menudo para solicitudes de red. Así que veamos un ejemplo ampliado de eso.
Usaremos el método de recuperación para cargar la información sobre el usuario desde el servidor remoto. Tiene muchos parámetros opcionales cubiertos en capítulos separados, pero la sintaxis básica es bastante simple:
dejar promesa = buscar (url);
Esto realiza una solicitud de red a la url
y devuelve una promesa. La promesa se resuelve con un objeto response
cuando el servidor remoto responde con encabezados, pero antes de que se descargue la respuesta completa .
Para leer la respuesta completa, debemos llamar al método response.text()
: devuelve una promesa que se resuelve cuando se descarga el texto completo del servidor remoto, con ese texto como resultado.
El siguiente código realiza una solicitud a user.json
y carga su texto desde el servidor:
buscar ('https://javascript.info/article/promise-chaining/user.json') // .luego se ejecuta a continuación cuando el servidor remoto responde .entonces(función(respuesta) { // respuesta.text() devuelve una nueva promesa que se resuelve con el texto de respuesta completo // cuando se carga devolver respuesta.text(); }) .entonces(función(texto) { // ...y aquí está el contenido del archivo remoto alerta(texto); // {"nombre": "iliakan", "isAdmin": verdadero} });
El objeto response
devuelto por fetch
también incluye el método response.json()
que lee los datos remotos y los analiza como JSON. En nuestro caso esto es aún más conveniente, así que pasemos a ello.
También usaremos funciones de flecha por razones de brevedad:
// igual que arriba, pero Response.json() analiza el contenido remoto como JSON buscar ('https://javascript.info/article/promise-chaining/user.json') .entonces(respuesta => respuesta.json()) .entonces(usuario => alerta(usuario.nombre)); // iliakán, tengo nombre de usuario
Ahora hagamos algo con el usuario cargado.
Por ejemplo, podemos realizar una solicitud más a GitHub, cargar el perfil de usuario y mostrar el avatar:
// Realizar una solicitud para user.json buscar ('https://javascript.info/article/promise-chaining/user.json') // cargarlo como json .entonces(respuesta => respuesta.json()) // Realizar una solicitud a GitHub .entonces(usuario => fetch(`https://api.github.com/users/${user.name}`)) // Carga la respuesta como json .entonces(respuesta => respuesta.json()) // Muestra la imagen del avatar (githubUser.avatar_url) durante 3 segundos (tal vez animarla) .entonces(githubUser => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesa-avatar-ejemplo"; documento.cuerpo.append(img); setTimeout(() => img.remove(), 3000); // (*) });
El código funciona; ver comentarios sobre los detalles. Sin embargo, hay un problema potencial en ello, un error típico de quienes empiezan a utilizar promesas.
Mire la línea (*)
: ¿cómo podemos hacer algo después de que el avatar haya terminado de mostrarse y se elimine? Por ejemplo, nos gustaría mostrar un formulario para editar a ese usuario o algo más. Por ahora no hay manera.
Para que la cadena sea extensible, debemos devolver una promesa que se resuelva cuando el avatar termine de mostrarse.
Como esto:
buscar ('https://javascript.info/article/promise-chaining/user.json') .entonces(respuesta => respuesta.json()) .entonces(usuario => fetch(`https://api.github.com/users/${user.name}`)) .entonces(respuesta => respuesta.json()) .then(githubUser => nueva Promesa(función(resolver, rechazar) { // (*) let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesa-avatar-ejemplo"; documento.cuerpo.append(img); setTimeout(() => { img.remove(); resolver (githubUser); // (**) }, 3000); })) // se activa después de 3 segundos .then(githubUser => alert(`Terminado de mostrar ${githubUser.name}`));
Es decir, el controlador .then
en la línea (*)
ahora devuelve new Promise
, que se liquida solo después de la llamada de resolve(githubUser)
en setTimeout
(**)
. El siguiente .then
en la cadena esperará por eso.
Como buena práctica, una acción asincrónica siempre debería devolver una promesa. Eso permite planificar acciones posteriores; Incluso si no planeamos extender la cadena ahora, es posible que la necesitemos más adelante.
Finalmente, podemos dividir el código en funciones reutilizables:
función cargarJson(url) { recuperación de retorno (url) .entonces(respuesta => respuesta.json()); } función cargarGithubUser(nombre) { devolver loadJson(`https://api.github.com/users/${name}`); } función mostrarAvatar(githubUser) { devolver nueva Promesa(función(resolver, rechazar) { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promesa-avatar-ejemplo"; documento.cuerpo.append(img); setTimeout(() => { img.remove(); resolver (githubUser); }, 3000); }); } // Úsalos: loadJson('https://javascript.info/article/promise-chaining/user.json') .entonces(usuario => loadGithubUser(nombre.usuario)) .entonces(mostrarAvatar) .then(githubUser => alert(`Terminado de mostrar ${githubUser.name}`)); //...
Si un controlador .then
(o catch/finally
, no importa) devuelve una promesa, el resto de la cadena espera hasta que se establezca. Cuando lo hace, su resultado (o error) se transmite más.
Aquí hay una imagen completa:
¿Son iguales estos fragmentos de código? En otras palabras, ¿se comportan de la misma manera en cualquier circunstancia, para cualquier función de controlador?
promesa.entonces(f1).catch(f2);
Versus:
promesa.entonces(f1, f2);
La respuesta corta es: no, no son iguales :
La diferencia es que si ocurre un error en f1
, .catch
lo maneja aquí:
promesa .entonces(f1) .catch(f2);
…Pero no aquí:
promesa .entonces(f1, f2);
Esto se debe a que se transmite un error a lo largo de la cadena y en el segundo fragmento de código no hay ninguna cadena debajo de f1
.
En otras palabras, .then
pasa resultados/errores al siguiente .then/catch
. Entonces, en el primer ejemplo, hay un catch
a continuación, y en el segundo no lo hay, por lo que el error no se controla.