Imagina que eres un cantante destacado y los fans preguntan día y noche por tu próxima canción.
Para obtener algo de alivio, promete enviárselo cuando se publique. Les das a tus fans una lista. Pueden completar sus direcciones de correo electrónico, de modo que cuando la canción esté disponible, todas las partes suscritas la reciban instantáneamente. E incluso si algo sale muy mal, digamos, un incendio en el estudio, y no puedes publicar la canción, igualmente serán notificados.
Todos están contentos: tú, porque la gente ya no te aglomera, y los fans, porque no se perderán la canción.
Esta es una analogía de la vida real de cosas que tenemos a menudo en programación:
Un “código de producción” que hace algo y lleva tiempo. Por ejemplo, algún código que carga los datos a través de una red. Eso es un "cantante".
Un “código consumidor” que quiere el resultado del “código productor” una vez que esté listo. Muchas funciones pueden necesitar ese resultado. Estos son los "fanáticos".
Una promesa es un objeto JavaScript especial que vincula el "código productor" y el "código consumidor". En términos de nuestra analogía: esta es la "lista de suscripción". El "código de producción" toma el tiempo necesario para producir el resultado prometido, y la "promesa" hace que ese resultado esté disponible para todo el código suscrito cuando esté listo.
La analogía no es muy precisa, porque las promesas de JavaScript son más complejas que una simple lista de suscripción: tienen características y limitaciones adicionales. Pero para empezar está bien.
La sintaxis del constructor de un objeto de promesa es:
let promesa = nueva promesa (función (resolver, rechazar) { // ejecutor (el código productor, "cantante") });
La función pasada a new Promise
se llama ejecutor . Cuando se crea new Promise
, el ejecutor se ejecuta automáticamente. Contiene el código de producción que eventualmente debería producir el resultado. En términos de la analogía anterior: el albacea es el “cantante”.
Sus argumentos resolve
y reject
son devoluciones de llamada proporcionadas por el propio JavaScript. Nuestro código solo está dentro del ejecutor.
Cuando el ejecutor obtenga el resultado, ya sea pronto o tarde, no importa, debería llamar a una de estas devoluciones de llamada:
resolve(value)
: si el trabajo finaliza correctamente, con value
del resultado.
reject(error)
: si se ha producido un error, error
es el objeto del error.
Para resumir: el ejecutor se ejecuta automáticamente e intenta realizar un trabajo. Cuando finaliza el intento, llama resolve
si tuvo éxito o reject
si hubo un error.
El objeto promise
devuelto por el new Promise
tiene estas propiedades internas:
state
: inicialmente "pending"
, luego cambia a "fulfilled"
cuando se llama resolve
o "rejected"
cuando se llama reject
.
result
: inicialmente undefined
, luego cambia a value
cuando se llama resolve(value)
o error
cuando se llama reject(error)
.
Entonces, el ejecutor eventualmente mueve promise
a uno de estos estados:
Más adelante veremos cómo los “fans” pueden suscribirse a estos cambios.
Aquí hay un ejemplo de un constructor de promesa y una función ejecutora simple con "producción de código" que lleva tiempo (a través de setTimeout
):
let promesa = nueva promesa (función (resolver, rechazar) { // la función se ejecuta automáticamente cuando se construye la promesa // después de 1 segundo indica que el trabajo ha terminado con el resultado "hecho" setTimeout(() => resolver("hecho"), 1000); });
Podemos ver dos cosas ejecutando el código anterior:
El ejecutor es llamado automática e inmediatamente (mediante new Promise
).
El ejecutor recibe dos argumentos: resolve
y reject
. Estas funciones están predefinidas por el motor JavaScript, por lo que no es necesario crearlas. Sólo debemos llamar a uno de ellos cuando esté listo.
Después de un segundo de "procesamiento", el ejecutor llama resolve("done")
para producir el resultado. Esto cambia el estado del objeto promise
:
Ese fue un ejemplo de finalización exitosa de un trabajo, una “promesa cumplida”.
Y ahora un ejemplo del ejecutor rechazando la promesa con un error:
let promesa = nueva promesa (función (resolver, rechazar) { // después de 1 segundo señaliza que el trabajo ha finalizado con un error setTimeout(() => rechazar(nuevo Error("¡Ups!")), 1000); });
La llamada a reject(...)
mueve el objeto de promesa al estado "rejected"
:
En resumen, el ejecutor debe realizar un trabajo (generalmente algo que lleva tiempo) y luego llamar resolve
o reject
para cambiar el estado del objeto de promesa correspondiente.
Una promesa que se resuelve o se rechaza se denomina "resuelta", a diferencia de una promesa inicialmente "pendiente".
Sólo puede haber un único resultado o un error.
El ejecutor debe pedir sólo una resolve
o un reject
. Cualquier cambio de estado es definitivo.
Todas las demás llamadas de resolve
y reject
se ignoran:
let promesa = nueva promesa (función (resolver, rechazar) { resolver("hecho"); rechazar(nuevo Error("...")); // ignorado setTimeout(() => resolver("...")); // ignorado });
La idea es que un trabajo realizado por el ejecutor pueda tener un solo resultado o un error.
Además, resolve
/ reject
espera solo un argumento (o ninguno) e ignorará argumentos adicionales.
Rechazar con objetos Error
En caso de que algo salga mal, el ejecutor debe llamar reject
. Eso se puede hacer con cualquier tipo de argumento (como resolve
). Pero se recomienda utilizar objetos Error
(u objetos que heredan de Error
). El razonamiento para ello pronto se hará evidente.
Llamar inmediatamente resolve
/ reject
En la práctica, un ejecutor generalmente hace algo de forma asincrónica y llama resolve
/ reject
después de un tiempo, pero no es necesario. También podemos llamar resolve
o reject
inmediatamente, así:
let promesa = nueva promesa (función (resolver, rechazar) { // no tomarnos nuestro tiempo para hacer el trabajo resolver(123); // inmediatamente damos el resultado: 123 });
Por ejemplo, esto puede suceder cuando empezamos a hacer un trabajo pero luego vemos que todo ya se ha completado y almacenado en caché.
Está bien. Inmediatamente tenemos una promesa resuelta.
El state
y result
son internos.
El state
de las propiedades y result
del objeto Promise son internos. No podemos acceder a ellos directamente. Podemos usar los métodos .then
/ .catch
/ .finally
para eso. Se describen a continuación.
Un objeto Promise sirve como enlace entre el ejecutor (el "código productor" o "cantante") y las funciones consumidoras (los "fanáticos"), que recibirán el resultado o error. Las funciones de consumo se pueden registrar (suscribir) utilizando los métodos .then
y .catch
.
El más importante y fundamental es .then
.
La sintaxis es:
promesa.entonces( function(resultado) { /* manejar un resultado exitoso */ }, function(error) { /* manejar un error */ } );
El primer argumento de .then
es una función que se ejecuta cuando se resuelve la promesa y recibe el resultado.
El segundo argumento de .then
es una función que se ejecuta cuando se rechaza la promesa y recibe el error.
Por ejemplo, aquí hay una reacción a una promesa resuelta exitosamente:
let promesa = nueva promesa (función (resolver, rechazar) { setTimeout(() => resolver("¡hecho!"), 1000); }); // resolver ejecuta la primera función en .luego promesa.entonces( resultado => alerta(resultado), // muestra "¡hecho!" después de 1 segundo error => alerta(error) // no se ejecuta );
Se ejecutó la primera función.
Y en caso de rechazo, la segunda:
let promesa = nueva promesa (función (resolver, rechazar) { setTimeout(() => rechazar(nuevo Error("¡Ups!")), 1000); }); // rechazar ejecuta la segunda función en .entonces promesa.entonces( resultado => alerta(resultado), // no se ejecuta error => alerta(error) // muestra "Error: ¡Ups!" después de 1 segundo );
Si solo nos interesan las terminaciones exitosas, entonces podemos proporcionar solo un argumento de función para .then
:
let promesa = nueva promesa (resolver => { setTimeout(() => resolver("¡hecho!"), 1000); }); promesa.entonces(alerta); // muestra "¡hecho!" después de 1 segundo
Si solo nos interesan los errores, podemos usar null
como primer argumento: .then(null, errorHandlingFunction)
. O podemos usar .catch(errorHandlingFunction)
, que es exactamente lo mismo:
let promesa = nueva Promesa((resolver, rechazar) => { setTimeout(() => rechazar(nuevo Error("¡Ups!")), 1000); }); // .catch(f) es lo mismo que promesa.entonces(null, f) promesa.catch(alerta); // muestra "Error: ¡Ups!" después de 1 segundo
La llamada .catch(f)
es un análogo completo de .then(null, f)
, es solo una abreviatura.
Al igual que hay una cláusula finally
en un try {...} catch {...}
normal, finally
hay promesas.
La llamada .finally(f)
es similar a .then(f, f)
en el sentido de que f
se ejecuta siempre, cuando se resuelve la promesa: ya sea que se resuelva o se rechace.
La idea de finally
es configurar un controlador para realizar la limpieza/finalización una vez completadas las operaciones anteriores.
Por ejemplo, detener los indicadores de carga, cerrar las conexiones que ya no son necesarias, etc.
Piense en ello como un cierre de fiesta. No importa si una fiesta fue buena o mala, cuántos amigos había en ella, todavía necesitamos (o al menos deberíamos) hacer una limpieza después.
El código puede verse así:
nueva Promesa((resolver, rechazar) => { /* hacer algo que lleve tiempo y luego llamar a resolver o tal vez rechazar */ }) // se ejecuta cuando se cumple la promesa, no importa si se ha realizado correctamente o no .finally(() => detener la carga del indicador) // para que el indicador de carga siempre se detenga antes de continuar .entonces(resultado => mostrar resultado, err => mostrar error)
Tenga en cuenta que finally(f)
no es exactamente un alias de then(f,f)
.
Hay diferencias importantes:
Un controlador finally
no tiene argumentos. finally
no sabemos si la promesa tiene éxito o no. Esto está bien, ya que nuestra tarea suele ser realizar procedimientos de finalización "generales".
Mire el ejemplo anterior: como puede ver, el controlador finally
no tiene argumentos y el siguiente controlador maneja el resultado de la promesa.
Un controlador finally
"pasa" el resultado o error al siguiente controlador adecuado.
Por ejemplo, aquí el resultado se pasa finally
a then
:
nueva Promesa((resolver, rechazar) => { setTimeout(() => resolver("valor"), 2000); }) .finally(() => alert("Promesa lista")) // se activa primero .entonces(resultado => alerta(resultado)); // <-- .luego muestra "valor"
Como puede ver, el value
devuelto por la primera promesa finally
se pasa a la then
.
Eso es muy conveniente, porque finally
no está destinado a procesar un resultado prometido. Como se dijo, es un lugar para realizar una limpieza genérica, sin importar cuál haya sido el resultado.
Y aquí hay un ejemplo de un error, para que veamos cómo se pasa finally
para catch
:
nueva Promesa((resolver, rechazar) => { lanzar nuevo Error("error"); }) .finally(() => alert("Promesa lista")) // se activa primero .catch(err => alerta(err)); // <-- .catch muestra el error
Un controlador finally
tampoco debería devolver nada. Si es así, el valor devuelto se ignora silenciosamente.
La única excepción a esta regla es cuando un controlador finally
arroja un error. Luego, este error pasa al siguiente controlador, en lugar de cualquier resultado anterior.
Para resumir:
Un controlador finally
no obtiene el resultado del controlador anterior (no tiene argumentos). En cambio, este resultado se pasa al siguiente controlador adecuado.
Si un controlador finally
devuelve algo, se ignora.
Cuando finally
arroja un error, la ejecución va al controlador de errores más cercano.
Estas características son útiles y hacen que las cosas funcionen de la manera correcta si finally
usamos como se supone que deben usarse: para procedimientos de limpieza genéricos.
Podemos adjuntar controladores a promesas establecidas.
Si hay una promesa pendiente, los controladores .then/catch/finally
esperan su resultado.
A veces, puede ser que una promesa ya esté resuelta cuando le agregamos un controlador.
En tal caso, estos controladores simplemente se ejecutan inmediatamente:
// la promesa se resuelve inmediatamente después de la creación let promesa = nueva Promesa(resolver => resolver("¡hecho!")); promesa.entonces(alerta); // ¡hecho! (aparece ahora mismo)
Tenga en cuenta que esto hace que las promesas sean más poderosas que el escenario de la “lista de suscripción” de la vida real. Si el cantante ya lanzó su canción y luego una persona se registra en la lista de suscripción, probablemente no recibirá esa canción. Las suscripciones en la vida real deben realizarse antes del evento.
Las promesas son más flexibles. Podemos agregar controladores en cualquier momento: si el resultado ya está ahí, simplemente se ejecutan.
A continuación, veamos ejemplos más prácticos de cómo las promesas pueden ayudarnos a escribir código asincrónico.
Tenemos la función loadScript
para cargar un script del capítulo anterior.
Aquí está la variante basada en devolución de llamada, sólo para recordárnoslo:
función loadScript(src, devolución de llamada) { let script = document.createElement('script'); script.src = src; script.onload = () => devolución de llamada (nulo, script); script.onerror = () => callback(new Error(`Error de carga de script para ${src}`)); documento.head.append(guion); }
Reescribámoslo usando Promesas.
La nueva función loadScript
no requerirá una devolución de llamada. En su lugar, creará y devolverá un objeto Promise que se resolverá cuando se complete la carga. El código externo puede agregarle controladores (funciones de suscripción) usando .then
:
función cargarScript(src) { devolver nueva Promesa(función(resolver, rechazar) { let script = document.createElement('script'); script.src = src; script.onload = () => resolver(script); script.onerror = () => rechazar(new Error(`Error de carga de script para ${src}`)); documento.head.append(guion); }); }
Uso:
let promesa = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promesa.entonces( script => alerta(`${script.src} está cargado!`), error => alerta(`Error: ${error.message}`) ); promesa.entonces(script => alert('Otro controlador...'));
Inmediatamente podemos ver algunos beneficios sobre el patrón basado en devolución de llamada:
Promesas | Devoluciones de llamada |
---|---|
Las promesas nos permiten hacer las cosas en el orden natural. Primero, ejecutamos loadScript(script) y .then escribimos qué hacer con el resultado. | Debemos tener una función callback a nuestra disposición al llamar loadScript(script, callback) . En otras palabras, debemos saber qué hacer con el resultado antes de llamar loadScript . |
Podemos .then una Promesa tantas veces como queramos. Cada vez, agregamos un nuevo "fan", una nueva función de suscripción, a la "lista de suscripciones". Más sobre esto en el próximo capítulo: Encadenamiento de promesas. | Sólo puede haber una devolución de llamada. |
Entonces las promesas nos brindan un mejor flujo de código y flexibilidad. Pero hay más. Lo veremos en los próximos capítulos.
¿Cuál es el resultado del siguiente código?
let promesa = nueva promesa (función (resolver, rechazar) { resolver(1); setTimeout(() => resolver(2), 1000); }); promesa.entonces(alerta);
El resultado es: 1
.
La segunda llamada a resolve
se ignora, porque solo se tiene en cuenta la primera llamada a reject/resolve
. Se ignoran más llamadas.
La función incorporada setTimeout
utiliza devoluciones de llamada. Cree una alternativa basada en promesas.
La función delay(ms)
debería devolver una promesa. Esa promesa debería resolverse después de ms
milisegundos, para que podamos agregarle .then
, así:
retardo de función (ms) { //tu código } delay(3000).then(() => alert('se ejecuta después de 3 segundos'));
retardo de función (ms) { devolver nueva Promesa(resolver => setTimeout(resolver, ms)); } delay(3000).then(() => alert('se ejecuta después de 3 segundos'));
Tenga en cuenta que en esta tarea se llama resolve
sin argumentos. No devolvemos ningún valor por delay
, solo garantizamos el retraso.
Vuelva a escribir la función showCircle
en la solución de la tarea Círculo animado con devolución de llamada para que devuelva una promesa en lugar de aceptar una devolución de llamada.
El nuevo uso:
mostrarCírculo(150, 150, 100).luego(div => { div.classList.add('bola de mensajes'); div.append("¡Hola mundo!"); });
Tome la solución de la tarea Círculo animado con devolución de llamada como base.
Abra la solución en una caja de arena.