Promesas en JavaScript

Promesas en JavaScript

Promesas en JavaScript

Aprovechando que hace poco ha sido San Valentín, hoy vamos a ver en qué consisten las promesas en JavaScript.

Las promesas se introdujeron en ECMAscript 6 y, básicamente, nos ofrecen una forma más limpia de trabajar con tareas asíncronas.

¿Qué es una tarea asíncrona?

En JavaScript existen muchas tareas que son asíncronas. Un ejemplo sencillo es la tarea de cargar una imagen.

var img = new Image();
img.src = "sample1.jpg";
document.write("Tamaño de la imagen: " + img.width + "x" + img.height);

El anterior código no funcionará de forma correcta. Esto es así porque al cambiar el atributo src de la imagen, el navegador necesita tiempo para pedir la imagen solicitada al servidor, descargarla, y descomprimirla luego en memoria. Todo ese proceso puede llevar bastante tiempo, dependiendo de la calidad de la conexión y el tamaño de la imagen, pueden ser incluso varios segundos. Para que el código JavaScript no se quede bloqueado todo ese tiempo, la carga de la imagen se realiza de forma asíncrona; por lo que el código sigue ejecutándose aunque no se haya completado la carga de la imagen.

Para que el anterior código funcione correctamente, debemos de escribir algo como esto:

var img = new Image();
img.src = "sample1.jpg";
img.onload = function() {
    document.write("Tamaño de la imagen: " + img.width + "x" + img.height);
    document.close();
};

En este caso, solo intentaremos acceder a las propiedades de la imagen cuando estamos seguros de haber terminado la carga de la misma.

Esta forma de trabajar presenta rápidamente sus limitaciones cuando queremos ejecutar un código al terminar de cargar no una, sino múltiples imágenes. Lo mismo sucede cuando queremos encadenar varias tareas asíncronas una detrás de otra. Y la cosa se complica bastante más si además queremos controlar posibles errores y la forma de reaccionar ante ellos.

¿En qué consisten las promesas en JavaScript?

En una tarea asíncrona, como la carga de una imagen, existe la posibilidad de que al cabo de unos instantes la tarea de carga finalice con éxito, de que se produzca un error, o de que la tarea de carga no termine de completarse nunca. Como al tratarse de una tarea asíncrona no tenemos el resultado de la operación de forma inmediata, necesitamos algún modo de saber el resultado de la operación y reaccionar ante dicho resultado.

Las promesas en JavaScript son justamente eso; la promesa de que tendremos el resultado de una tarea asíncrona. Para crear una promesa debemos de crear un objeto de la clase Promise. Este objeto, representa un valor que podría, o no, estar disponible en el futuro. Y ese valor, es el resultado de una operación asíncrona.

Podemos crear una promesa del siguiente modo:

var promesa = new Promise(function(resolver, rechazar) {
  /* código de la función ejecutora */
});

Al crear el objeto de tipo Promise, debemos de indicar lo que se conoce como función ejecutora. Esta función es la que debe de ejecutar el código asíncrono, y tiene dos posibles parámetros; resolver (resolve) y rechazar (reject). Estos parámetros son callbacks, es decir, funciones que se pasan por parámetro y que se llamarán cuando se complete o falle la operación deseada. El código de la función ejecutora debe de llamar a la callback de resolver si todo ha ido bien, o a la callback de rechazar si no ha sido así. Es importante tener en cuenta que la función ejecutora debe de llamar solo a una de las dos callbacks.

En nuestro ejemplo, el código para crear la promesa podría ser el siguiente:

var promesa = new Promise(function(resolve, reject) {
    var img = new Image();
    img.src = "sample1.jpg";
    img.onload = function() { resolve(img); }
    img.onerror = function() { 
        reject(new Error("Error al cargar la imagen: " + img.src));
    }
});

Vamos a mejorarlo un poco, vamos a crear una función para cargar imágenes que nos devolverá una promesa por cada imagen que queramos cargar.

function loadImg(src) {
    return new Promise(function(resolve, reject) {
        var img = new Image();
        img.src = src;
        img.onload = function() { resolve(img); }
        img.onerror = function() { 
            reject(new Error("Error al cargar la imagen: " + img.src)); 
        }
    });
}

Para usar las promesas en JavaScript, debemos de “consumirlas”. Para ello podemos usar el método then().

var promesa = loadImg("sample1.jpg");
promesa.then(
    function(img) { 
        document.write("Tamaño: " + img.width + "x" + img.height); 
        document.close();
    },
    function(error) { 
        document.write(error.message); 
        document.close();
    }
);

El método then() acepta dos parámetros, que son dos funciones callback. La primera se ejecutará si la promesa se ha cumplido satisfactoriamente, y la segunda se ejecutará si se ha producido algún error.

Estas funciones tienen como parámetros los que le pasamos en el código de la función ejecutora. En el caso de la callback para el error, se recomienda que el primer parámetro sea un objeto de la clase Error, ya que cualquier error/excepción que se produzca en el código de la función ejecutora, provocará que al llamar al método then() se ejecute la callback del error.

Si no queremos gestionar los errores, no es necesario el segundo parámetro del método then().

Otra forma de gestionar los errores es con el método catch(), que solo acepta de parámetro una callback que se ejecutará cuando se produzca un error. Por lo que también podemos escribir el anterior código así:

var promesa = loadImg("sample1.jpg");
promesa.then(
    function(img) { 
        document.write("Tamaño: " + img.width + "x" + img.height); 
        document.close();
    }
).catch(
    function(error) { 
        document.write(error.message); 
        document.close();
    }
);

Existe la posibilidad de que queramos ejecutar un mismo código tanto si se cumple como si no se cumple la promesa. En ese caso, en lugar de los métodos then() o catch() podemos usar el método finally() (NOTA: este método es de EcmaScript 2018).

var promesa = loadImg("sample1.jpg");
promesa.then(
    function(img) { 
        document.write("Tamaño: " + img.width + "x" + img.height); 
    }
).catch(
    function(error) { 
        document.write(error.message);         
    }
).finally(
    function() {
        document.close();
    }
);

Es bastante frecuente el uso de funciones flecha cuando se usan promesas en JavaScript, por lo que el código completo que estamos viendo también podemos escribirlo de este modo:

function loadImg(src) {
    return new Promise(function(resolve, reject) {
        var img = new Image();
        img.src = src;
        img.onload = () => { resolve(img); }
        img.onerror = () => { 
            reject(new Error("Error al cargar la imagen: " + img.src));
        }
    });
}

var promesa = loadImg("sample1.jpg");
promesa.then(
    img => { document.write("Tamaño: " + img.width + "x" + img.height); }
).catch(
    error => { document.write(error.message); }
).finally(
    () => { document.close(); }
);

El poder encadenar un catch() después de un then(), es porque ambos métodos devuelven la promesa. Pero es posible hacer que se devuelva otra nueva promesa totalmente diferente; esto nos permite encadenar varias promesas, de forma que al cumplirse una, intentamos realizar la siguiente. Veamos un ejemplo:

// intentamos cargar la primera imagen
var promesa = loadImg("sample1.jpg");
promesa.then(
    img => { 
        // mostramos información de la primera imagen
        document.write("Tamaño: " + img.width + "x" + img.height + "<br>");
        // intentamos cargar la segunda imagen
        return loadImg("sample2.jpg");
    }
).then(
    img => { 
        // mostramos información de la segunda imagen
        document.write("Tamaño: " + img.width + "x" + img.height); 
        document.close();
    }
);

Esta forma de trabajar es muy frecuente cuando se usan promesas. Sin embargo, aunque esto puede ser muy útil cargando scripts o realizando otro tipo de peticiones a un servidor Web, no es habitual cuando queremos cargar imágenes.

Métodos estáticos de la clase Promise

Cuando cargamos imágenes el orden en el que se cargan suele ser irrelevante. Lo que si nos puede interesar es saber cuándo están todas las imágenes cargadas. Para averiguarlo, disponemos del método estático all() de la clase Promise. Este método espera como parámetro un objeto que se pueda iterar y que contenga promesas, como por ejemplo, un array de promesas. De forma similar a Promise.all(), podemos usar Promise.race() si solo nos interesa la primera promesa que se cumpla (o que dé un error).

Si queremos reaccionar ante la primera promesa que se cumpla o falle, usaremos el método race();

document.write("Cargando imágenes, por favor espera...");
Promise.race([
    loadImg("sample1.jpg"),
    loadImg("sample2.jpg"),
    loadImg("sample3.jpg"),
]).then(
    img => {
        document.write("La primera imagen que se ha cargado es: " + img.src); 
        document.close();
        // una vez cargada, la podemos mostrar en pantalla
        var imagen = document.createElement('img');
        imagen.src = img.src;
        document.body.appendChild(imagen);
    }
).catch(
    error => {
        document.write(error.message); 
        document.close();
    }
);

Si queremos reaccionar cuando se cumplan todas las promesas, usaremos el método all();

document.write("Cargando imágenes, por favor espera...");
var imagenes = ["sample1.jpg", "sample2.jpg", "sample3.jpg"];
Promise.all(imagenes.map(loadImg)).then(
    () => {
        document.write("Todas las imágenes se han cargado con éxito!"); 
        document.close();
        // una vez cargadas, las podemos mostrar en pantalla
        imagenes.forEach((src)=>{
            var img = document.createElement('img');
            img.src = src;
            document.body.appendChild(img);
        });
    }
).catch(
    error => {
        document.write(error.message); 
        document.close();
    }
);

Y nada más. Espero que este artículo os haya resultado interesante. En un futuro artículo veremos como aplicar las promesas para obtener otro tipo de recursos del servidor Web.

Escribe un comentario