¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Aprende ES6 - Las promesas

Este es ya el tercer artículo en el que hablamos de ES6. En artículos anteriores hemos hablado sobre let, const y los ámbitos y las funciones flecha. Si esto no te suena de nada y estás intentando ponerle algún sentido a las siglas, ¡no te preocupes! Echa un vistazo a nuestra Introducción a ES6.

Ha llovido mucho desde que escribí aquel artículo de "Comprendiendo promesas y deferreds en jQuery". ¡Muchísimo! Es un artículo de marzo de 2012 por lo que han pasado casi 4 años de carrera profesional por medio. A no ser que hayáis seguido usando promesas en Angular por ejemplo, me gustaría que desaprendierais un poco aquello, puesto que la sintaxis cambia.

Ahora paraos nun segundo para digerir esto: ¡Las promesas son ahora nativas en JavaScript!

¡Yay!

Este tema es bastante complejo de digerir, especialmente si no se tiene experiencia previa, por lo que vamos a tomárnoslo con calma.

¿Qué es una promesa?

Una promesa representa un valor que podremos gestionar en algún momento del futuro. Esto nos da una gran flexibilidad ya que nos valen tanto para código síncrono como código asíncrono. Las promesas nos garantizan dos cosas sobre el valor:

  • Ninguna función asociada a la promesa, puede modificar su valor. La promesa es inmutable. (Esto tiene sus matices)
  • Estamos totalmente seguros de que, en algún momento, recibiremos el valor sin importar cuándo registramos un gestor, incluso si ya está resuelta (al contrario que con los eventos).

Veámoslo más claro con un par de ejemplos para ver más claramente las diferencias.

Eventos

$(window).trigger('mi-evento', { foo: 'bar' });

// Ya hemos llegado tarde, mi-evento se ha ejecutado =(
$(window).on('mi-evento', resultado => {
  alert(resultado.foo);
});

Como ves, en este caso llegamos tarde a la ejecución de un evento por lo que la función que se encarga de gestionar el evento mi-evento nunca se ejecutará.

Promesas

// Una promesa que ya está resuelta
let promesa = Promise.resolve({  
  foo: 'bar'
}); 

// Aunque la promesa ya ha sido resuelta, podemos acceder al valor
promesa.then(resultado => alert(resultado.foo)); 

let otraPromesa = new Promise((resolve, reject) => {  
   setTimeout(() => resolve(73), 2000);
});

// La función no puede cambiar el valor
otraPromesa.then(resultado => {  
  resultado += 2;  
  console.log(resultado);
});

// Aun sigue siendo 73
otraPromesa.then(resultado => console.log(resultado));  

No nos centremos ahora mismo en la sintaxis, que veremos en un segundo. No obstante, como puedes ver en los comentarios, aunque una promesa ya haya sido resuelta, podemos acceder a su valor el cual no puede ser modificado.

Ahora que ya sabemos las diferencias más importantes entre Promesas y Eventos, podemos pasar a ver las promesas en más detalle.

La esencia de una promesa

La forma estándar de crear una Promesa es usando el constructor new Promise el cual acepta una función que recibe dos funciones como parámetros. El primer parámetro (normalmente llamado resolve) es una función que llamaremos cuando el valor futuro esté listo mientras que el segundo parámetro (normalmente llamado reject) es una función para rechazar la Promesa (si no puede obtener el valor).

let promesa = new Promise((resolve, reject) => {  
   if (/* condición */) {
      resolve(/* valor */);  // Resuelta con éxito
   } else {
      reject(/* motivo */);  // Error, rechazada
   }
});

Sabiendo esto, sabemos que la Promesa tiene uno de estos tres estados:

  • Pendiente: Hasta que una promesa sea resuelta/rechazada, está en estado pendiente.
  • Resuelta: Cuando la primera función es llamada, la Promesa se considera resuelta con el valor que se le proporcione.
  • Rechazada: Cuando se llama la segunda función, la Promesa se considera rechazada con el valor que se le proporcione.

Una promesa solo puede cambiar de estado una vez, de pendiente a uno de los dos estados y, tal y como ya hemos dicho, los consumidores de dicha promesa, no pueden cambiar su valor.

Tal y como hemos visto antes, es posible crear una Promesa ya resuelta usando el método Promise.resolve():

let promesaResuelta = Promise.resolve('foo');  

Consumiendo promesas

Una vez que ya hemos creado una Promesa, lo interesante viene cuando la consumimos, es decir, cuando queremos procesar el valor futuro que tendrá.

En vez de usar .on, las promesas tienen una interfaz algo diferente. Al igual que con los eventos, una promesa puede ser consumida tantas veces como quieras. En vez de .on, los métodos son .then y .catch:

let promesa = new Promise();

promesa.then(respuesta => {  
  // Gestionar respuesta
});

promesa.catch(error => {  
  // Gestionar Error
});

Merece la pena mencionar, que .then puede recibir un segundo parámetro, que hará de .catch:

let promesa = new Promise();

promesa.then(  
  respuesta => {
    // Gestionar respuesta
  },
  error => {
    // Gestionar error
  }
);

De la misma forma que podemos omitir el segundo parámetro de la función .then, podemos omitir el primero también usando .then(null, rechazo).

Then y Catch

.then y .catch son dos métodos realmente especiales ya que lo que devuelven esos métodos, son promesas con el valor que devolvamos en estas funciones. Esto nos permite encadenar varios .then, a la vez que transformamos el valor.

¡Pero espera! ¿No era que no se podía transformar el valor?

¡No! No se puede. No obstante esto tiene truco, porque hay una nueva promesa en juego. Veámoslo con un ejemplo:

let promesa = new Promise((resolve, reject) => {  
  setTimeout(() => {
    resolve(4);
  }, 2000);
});

promesa.then(resultado => resultado + 2)  
  .then(resultado => {
  console.log(resultado); // 6
})

promesa.then(resultado => {  
  console.log(resultado); // 4
});

Veamos lo que pasa poco a poco:

  1. Creamos una promesa que, tras 2 segundos, resolvemos con el valor 4.
  2. Consumimos la promesa con una función que devuelve, el resultado de sumar el resultado de la promesa (que sabemos que será 4) más el número 2. Dado que .then nos devuelve una promesa, encadenamos una nueva promesa con la que mostramos el resultado en la consola.
  3. Sobre la promesa original, volvemos a usar .then para, simplemente, mostrar el resultado.

Como ves, la promesa original siempre obtendrá el valor originalmente resuelto, 4, mientras que podemos ir encadenando modificaciones que nos vengan bien y que nos ofrezcan el valor que nos interesa.

Veamos un ejemplo más interesante en pseudo-código para ver un ejemplo más real:

function usuarioTieneEdadValida() {  
  return obtenerUsuario().then(respuesta => {
    return respuesta.data;
  }).then(respuesta => {
    return respuesta.edad > 18;
  });
}

usuarioTieneEdadValida().then(valido => {  
  if (valido) {
    alert('El usuario es válido!');
  } else {
    alert('El usuario no es válido');
  }
});

En este ejemplo con código inventado, tenemos una función llamada usuarioTieneEdadValida que usa una función obtenerUsuario. obtenerUsuario devuelve una promesa (supongamos que usa una API para obtener un usuario), de la cual obtenemos el elemento .data. En la siguiente función de la cadena, ya tenemos el objeto usuario y simplemente devolvemos si el usuario tiene más o menos de 18 años.

De esta forma, al usar usuarioTieneEdadValida, nos es totalmente transparente lo que se haya tenido que hacer por detrás para obtener el usuario, solo encadenamos un .then y ya obtendremos un valor que es true o false.

Componiendo promesas

Un escenario muy común, especialmente si usas Node, es depender de varias promesas antes de llevar a cabo una última tarea. Pongamos un ejemplo real:

Tenemos una pequeña aplicación en Node, que necesita un fichero de vídeo de una serie, uno de subtítulo y el nombre del capítulo para mezclarlos. Podríamos hacer el código así:

obtenerVideo().then(video => {  
  obtenerSubtitulo().then(subtitulo => {
    obtenerNombreCapitulo().then(nombre => {
      componerMezcla(video, subtitulo, nombre);
    });
  });
});

No obstante, esto hace que las cosas se ejecuten en serie. ¿Por qué querríamos esperar a tener el vídeo para hacernos con el subtítulo que posiblemente tengamos que descargarnos? Con Promise.all podemos componer promesas:

Promise.all([  
  obtenerVideo(),
  obtenerSubtitulo(),
  obtenerNombreCapitulo()
]).then(respuestas => {
  componerMezcla(respuestas[0], respuestas[1], respuestas[2]);
});

La variable respuestas es una matriz que contiene (en orden en que fueron llamadas), los valores ya resueltos de todas las promesas.

Es importante mencionar, que si una sola de las promesas es rechazada (o lanza un error), la Promesa de .all será rechazada también en el momento en que eso ocurra.

Promise.all([  
  Promise.reject(),
  obtenerVideo(),
  obtenerSubtitulo(),
  obtenerNombreCapitulo()
]).then(respuestas => {
  console.log('Todo conseguido');
});
// Nunca llegará al then

Sabiendo esto, resumimos sabiendo que Promise.all tiene dos posibles resultados:

  • Queda rechazada con un solo motivo si una de las promesas es rechazada.
  • Queda resuelta en el momento en que todas las promesas de las que depende, sean resueltas.

Hay veces que no tenemos que esperar a que todas las promesas se hayan completado y tan solo queremos obtener los resultados de la primera promesa en cumplirse. Para ello tenemos Promise.race() que, al igual que Promise.all() recibe una matriz de promesas; pero al contrario que Promise.all() se completa en cuanto una de las promesas se haya completado.

var promesa1 = new Promise((resolve, reject) => {  
  setTimeout(() => { resolve('Primero'); }, 4000);
});

var promesa2 = new Promise((resolve, reject) => {  
  setTimeout(() => { resolve('Segundo'); }, 2000);
});

Promise.race([promesa1, promesa2]).then(resolucion => {  
  console.log('Ganó:', resolucion);
});
// Ganó: Segundo

Si hay un rechazo, race acabará también y la promesa de race será rechazada.

Espero haberos aclarado cómo funcionan y cómo controlar las promesas en ES6. En otro artículo veremos cómo algunas funciones de ES6 usan las promesas, como fetch.

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!