Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:promises

¡Esta es una revisión vieja del documento!


Promises

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Introducción

Las promises (promesas) ya hace algún tiempo que se vienen utilizando en el desarrollo JavaScript, aunque no es hasta ECMAScript 2015 cuando se incorporan “de serie” en el lenguaje. De todos modos, si debes soportar algún navegador que no las incorpora (básicamente IE en cualquier versión, Edge -incluido en Windows 10- sí que las soporta) puedes usar algún polyfill.

La especificación de Promises en ECMAScript 2015 es conocida como Promises/A+

¿Qué es una promise?

Una promise es, como su propio nombre indica, una promesa: una promesa de que cierta tarea se finalizará en algún momento. Así pues, debes ver a las promises como tareas “que serán ejecutadas en algún momento dado”. Esto incluye el pasado, es decir, una promise puede representar una tarea realizada, realizándose o por realizar (pero que se realizará).

A priori puede costar ver la necesidad de tener algo como las promises, pero para ver su potencial es necesario entender antes el concepto de la “pirámide de callbacks”.

La pirámide de callbacks

Tomemos como punto de partida el siguiente código, ¡a ver si encuentras qué tiene de malo!:

navigator.geolocation.getCurrentPosition(function(c) { 
    console.log(c);
}
);

La respuesta es: nada. Este código no tiene nada de malo. Estamos llamando al método getCurrentPosition y pasándole una función de callback. Hacemos esto continuamente en JavaScript. El problema puede venir si dentro de la función de callback tenemos que llamar a otra función que espera a su vez un callback, tal y como sucede en el siguiente código:

navigator.geolocation.getCurrentPosition(function(c) { 
        console.log(c);
        window.setTimeout(function() {
            navigator.geolocation.getCurrentPosition(function(c2) {
                console.log(c2); 
            });
        }, 2000);
    }
);

callback: Un método que se llama automáticamente al terminar de ejecutarse otro método anterior. Por ejemplo, se llama a una función que procesa unos datos y cuando ésta termina, llama a un método que le hayamos indicado en sus argumentos, para iniciar un proceso diferente o para indicar que ha finalizado.

Observa que dentro del callback del primer getCurrentPosition se llama a setTimeout que recibe otro callback, y a su vez dentro de este segundo callback se llama otra vez a getCurrentPosition y se le pasa una tercera función de callback.

Si te fijas en la estructura de código, debido a la indentación, parece una pirámide con la punta hacia la derecha. Esta sucesión de unos callbacks dentro de otros una y otra vez es lo que se conoce como la “pirámide de callbacks” y puede llegar a extremos enfermizos:

Fuente de la imagen

Las promises pueden ayudar a solventar este problema, aunque se debe evitar caer en la “pirámide de promises”, que también existe. Pero también veremos otras posibilidades que ofrecen.

Desmontando la pirámide de callbacks

Vamos a ver cómo el uso de promises nos permite desmontar la pirámide de callbacks que teníamos en el ejemplo anterior. Para ello vamos a suponer que las APIs que usábamos están implementadas usando promises. Simplemente, ten presente que esto no es cierto: no hay todavía APIs nativas en JavaScript que estén implementadas usando promises.

Para este ejemplo vamos a suponer que existen las funciones getCurrentPositionPromise y setIntervalPromises que hacen lo mismo que las funciones reales getCurrentPosition y setInterval pero usando promises. Más adelante veremos cómo podemos crear realmente esas funciones.

Una API basada en promises en lugar de esperar un callback devuelve una promise. Así pues, nuestro supuesto método getCurrentPosition no esperaría un parámetro con la función de callback, sino que devolvería una promise, por lo que sería llamado de la siguiente manera:

var pr = navigator.geolocation.getCurrentPositionPromise();

El valor devuelto (pr) es la promise. Recuerda que una promise representa una tarea que ya puede estar hecha o que se realizará en un tiempo futuro. Para saber el estado de la tarea debemos consultar el estado de la promise, el cual puede ser:

  • Pendiente (pending): La tarea no se ha ejecutado y no hay resultado alguno.
  • Completada (fulfilled): La tarea ha sido completada y por lo tanto hay un resultado obtenido. Este resultado (que puede ser undefined) queda asociado a la promise.
  • Error (rejected): Ha habido un error ejecutando la tarea y por lo tanto no hay resultado alguno que podamos consultar.

En realidad no existe ninguna propiedad que nos diga el estado en el que se encuentra una promise. En su lugar se usan dos métodos de la promise: el método then y el método catch.

El primero, then, recibe como parámetro una función, que se ejecutará en cuanto la tarea sea completada sin error. Por su parte, el segundo método, catch, recibe como parámetro una función que se ejecutará si hay algún error al ejecutar la tarea.

Para entendernos, cuando la promise pasa de estado “Pendiente” a “Completada” se ejecutará la función pasada como parámetro en el método then, y cuando la promise pasa de “Pendiente” a “Error” se ejecutará la función pasada como parámetro en el catch.

Pero no solo eso, si llamas a then o a catch de una promise que ya se encuentra en estado “Completada” o “Error”, se ejecutará la función pasada correspondiente. Es decir, da igual si la tarea representada por la promise ha terminado o no cuando llames a then: si no lo ha hecho, tan pronto termine se ejecutará la función parámetro del then, y si ya había finalizado entonces se ejecutará de inmediato la función pasada como argumento en el then.

Por lo tanto para simular el ejercicio anterior con promises usaríamos un código similar a:

var pr = navigator.geolocation.getCurrentPositionPromise();
pr.then(function(coords) {
    console.log(coords);
    var pr2 = setIntervalPromises(2000);
    pr2.then(function() {
        var pr3 = navigator.geolocation.getCurrentPositionPromise();
        pr3.then(function(coords2) {
            console.log(coords2);
        });
    });
});

Observa que la función que se le pasa al then recibe como argumento el valor devuelto por la tarea representada por la promise.

Este código, sin embargo, no es mucho mejor que el anterior. Es cierto que ahora no tenemos una “pirámide de callbacks”, pero tenemos una “pirámide de promises”. Esta otra pirámide se da cuanto tenemos thens dentro de thens dentro de thens de forma sucesiva (en este caso encadenamos tres thens uno dentro del otro). Eso es porque en este ejemplo estamos usando mal las promises.

Evita siempre caer en la pirámide de promises: tiene los mismos problemas de legibilidad y mantenibilidad que la pirámide de callbacks.

Rompiendo la pirámide de promises

Para poder romper la pirámide de promises el primer paso es separar cada una de las tareas en funciones separadas:

// Tarea 1: Obtener la posición (e imprimirla en consola)
var task1 = function() {
    var pr = navigator.geolocation.getCurrentPositionPromise();
    pr.then(function(c) {
       console.log('1ª coord -> ', c); 
    });
    return pr;
}
 
// Tarea 2: Esperar 2000ms (y imprimir por consola una vez han pasado)
var task2 = function() {
    var pr = setIntervalPromises(2000);
    pr.then(function() {
       console.log('2000 ms de espera'); 
    });
    return pr;
}
 
// Tarea 3: Obtener la posición (e imprimirla en consola)
var task3 = function() {
    var pr = navigator.geolocation.getCurrentPositionPromise();
    pr.then(function(c) {
        console.log('2ª coord -> ', c);
    });
    return pr;
}

Observa que las funciones hacen lo que tengan que hacer, pero devuelven una promise, puesto que representan tareas. En este caso nos limitamos a devolver la misma promise que nos devuelven los métodos getCurrentPositionPromise y setIntervalPromises.

Ahora necesitamos encadenar las 3 tareas una tras otra. Para ello, no debemos usar un código como el siguiente:

var pr = task1().then(
            function() {
                task2().then(function() {
                    task3();
                });
            });

¡Este código funciona correctamente pero sigues teniendo la pirámide de promises! El mero hecho de tener las tareas separadas en funciones no te sirve para evitar la pirámide de promises si luego vuelves a encadenar los thens uno dentro de otro.

Para poder romper la pirámide de promises debes tener presente una cosa: los métodos then se pueden encadenar entre ellos. De hecho el método then devuelve otra promise cuyo estado depende del valor devuelto por la función que se pasa al then. Así podemos tener el siguiente código:

task1().
    then(task2).
    then(task3).
    then(function() {
        console.log('Todas las Promises resueltas.');
    });

Observa la elegancia de este código: ya no hay thens dentro de thens, ni nada parecido. En su lugar el código es muy claro y descriptivo. Puedes leerlo de la siguiente manera: ejecutar task1 y luego task2 y luego task3 y cuando task3 haya terminado ejecutas el último then.

¡Más claro y sencillo, no puede ser!

Este código funciona porque cada método then devuelve una promise, por esto los podemos encadenar.

Pero te estarás preguntando: ¿cuándo se resuelve la promise que devuelve el método then?. Pues la respuesta es muy simple:

  1. En el caso de que la función pasada al método then devuelva una promise, entonces cuando se resuelva esta última se resolverá también la promise devuelta por el método then. A todos los efectos es como si then retornase la misma promise que retorna la función que se le pasa como parámetro. A este hecho se le conoce como promise unwrapping.
  2. En el caso de que la función pasada a then no devuelva una promise entonces la promise devuelta por then estará en estado resuelta.
  3. En el caso de que la función que se pasa como parámetro a then lance un error, entonces la promise devuelta por then estará en estado de error.

Volvamos a ver el código que encadena los thens pero ahora con comentarios para entender mejor qué ocurre a cada paso:

task1(). // En este punto tenemos una promise, llamémosla 'pr1'
    then(task2). // llamamos a then de pr1. Esto ejecutará pr2.
                 // task2 devuelve una promise, llamémosla pr2.
                 // En este caso el método then devuelve otra promise cuyo estado depende
                 // del estado de pr2. Es como si el método then devolviese la misma pr2.
    then(task3). // Por lo tanto esta línea equivale a pr2.then(...). Por lo que este then
                 // se ejecutará cuando pr2 se haya resuelto.
                 // Igualmente task3 devuelve una promise (llamémosla pr3),
                 // por lo que el método then(task3) devuelve una promise
                 // cuyo estado depende de pr3
    then(function() { // Este then pues se ejecutará  cuando pr3 se haya resuelto.
        console.log('Todas las Promises resueltas.');
    });

Encadenamiento de then

Vamos a ver algunos ejemplos del encadenamiento de métodos then que hemos comentado. Para cada ejemplo hay una captura de pantalla de la consola de Firefox para que veas el estado de los objetos.

El primer ejemplo muestra como then devuelve siempre una promise:

var pr = new Promise(function(r,j) { r(); });
var p1 = pr.then(function() {
    console.log('promise resolved');
});

then devuelve una promise

Observa como p1 es una promise a pesar de que la función pasada por then no devuelve nada. Por ello la promise p1 está resuelta y su valor asociado es undefined

En este segundo ejemplo se puede observar como la promise devuelta por then está en estado de error si la función que se pasa al then lanza una excepción:

var pr = new Promise(function(r,j) { r(); });
var p2 = pr.then(function() {
    throw("some error!");
});

then devuelve una promise con error

Y el tercer ejemplo muestra que, si la función que se pasa a then devuelve una promise, la promise devuelta por then se resuelve en cuanto se resuelve la promise devuelta por la función que se pasa a then:

var pr = new Promise(function(r,j) { r(); });
var p1 = pr.then(function() {
    return new Promise(function(r,j) { 
        window.setTimeout(function() {
            console.log("promise inside then is resolved");
            r();
        }, 5000);
    });
});
var p2=p1.then(function() { console.log("p1 resolved too");});

Hasta que no se resuelva la promise “interna” (cuando pasan los 5 segundos del setTimeout) no se resuelve p1 (la promise devuelta por el then), de ahí que salga primero el mensaje “promise inside then is resolved” y luego el mensaje “p1 resolved too”:

la promise de then depende de la promise interna

Con estos ejemplos espero que quede más claro cómo funciona el encadenamiento de promises y cómo el método then devuelve siempre una promise.

Creación de promises

Vamos a ver cómo crear nuestras propias funciones que utilicen y devuelvan promises. Para ello debemos ver cómo crear objetos del tipo Promise.

La función constructora Promise acepta un solo parámetro, que se conoce como la “función ejecutora”. Dicha función contiene el código de la tarea que la promise encapsula:

var p = new Promise(function() { 
    console.log('Hello promises');
});

El código que se coloca dentro de la función ejecutora se ejecuta inmediatamente. Es decir, el código anterior imprime la cadena “Hello promises” por la consola. El objeto p es la promise. En este caso este código sirve de poco porque la promise p está en estado pendiente, y no hay manera de resolverla (pasarla a estado completado) o rechazarla (pasarla a estado de error).

El resolver o rechazar una promise debe hacerse desde dentro de la función ejecutora.

Dicha función recibe dos parámetros (por regla general llamados resolve y reject) que son sendas funciones que al llamarlas nos permiten resolver o rechazar la promise. P. ej. el siguiente código devuelve una promise completada:

var p = new Promise(function(resolve, reject) { 
    console.log('Hello promises');
    resolve();
});

En este punto, la variable p es una promise en estado completado. Así podríamos tener una función foo que nos permitiese obtener promises (ya completadas):

var foo = function() {
    var p = new Promise(function(resolve, reject) { 
        console.log('Hello promises');
        resolve();
    }); 
    return p;
};

Y ahora podríamos tener el siguiente código:

foo().then(function() { console.log('Hello after promises')});

Esto imprimiría por la consola primero la cadena “Hello promises” y luego la cadena “Hello after promises”. Fíjate en que foo devuelve la promise (ya completada), por lo que la siguiente llamada a then se ejecuta inmediatamente. Por supuesto este ejemplo es muy sencillo y usar promises en este escenario no aporta nada. Lo interesante viene cuando la función foo espera un callback, por la razón que sea. Los callbacks suelen usarse cuando hay operaciones asíncronas aunque no siempre tiene porque ser así.

Encapsulando APIs asíncronas con promises

Vamos a ver un ejemplo más interesante. En este caso encapsularemos la API getCurrentPosition para usar promises en lugar de callbacks. Primero definimos nuestra nueva función, que llamaremos getCurrentPositionPromise. Esta función llamará a getCurrentPosition y le pasará un callback. Dentro de éste resolveremos la promise. Parece complicado así explicado pero es muy simple:

navigator.geolocation.getCurrentPositionPromise = function() {
    var pr = new Promise(function(resolve, reject) {
       navigator.geolocation.getCurrentPosition(function(c) {
           resolve(c);  // Resolvemos la promise 
       });
    });
     return pr;   // Devolvemos la promise
}

La función getCurrentPositionPromise envuelve a la función nativa getCurrentPosition usando promises en lugar de callbacks. Por lo tanto ahora podemos tener el siguiente código:

var task = navigator.geolocation.getCurrentPositionPromise();
task.then(function(c) { 
    console.log("Done -> ",c);
});

Ejercicio: Completa la función getCurrentPositionPromise para que tenga presente que la función nativa getCurrentPosition puede dar un error y en este caso que pase la promise al estado de error (llamando a reject). En el fichero setInterval. Tienes la solución en el fichero encapsular2.js.

Fichero encapsular2.js:

var setIntervalPromises = function(interval) {
    var pr = new Promise(function(resolve, reject) {
        window.setInterval(function() { resolve(); }, 
            interval);
    });
 
     return pr;   // Devolvemos la promise
}
 
// uso: 
// var wait = setIntervalPromises(interval)
// wait.then(function() { ... });

DEMO: Creación y uso de promises

Partimos de lo siguiente:

var demo = function(res, cb) {
    window.setTimeout(function() { cb(res); }, 2000 );
}

Para invocarla:

demo(10, function(r) {
    console.log("Recibido ->", r);
});

Vamos a transformar esa función para utilizar una promise y así no tener que usar callbacks.

var demo = function(res) {
 
    var pr = new Promise(function(resolve, reject) {
        window.setTimeout(function() { resolve(res); }, 2000 );
    });
 
    return pr;
}

Para utilizarla:

var task = demo(10);
 
task.then(function(r) {
    console.log("Recibido ->", r);
})

El resultado será el mismo que antes, pero ya no hay ninguna función de callback y queda más separado la llamada de la función de lo que ocurre cuando la función finaliza.

DEMO: Encadenar promises

Veamos cómo podemos hacer una secuencia de promises y se ejecuten una tras otra.

Partimos de la siguiente función que esperaba un tiempo y luego resuelve la promise con un resultado.

var demo = function(res) {
    var pr = new Promise(function(resolve, reject) {
        if (res < 0) {
            reject(new Error("El resultado debe ser positivo"));
        }
        window.setTimeout(function() { resolve(res)}, 2000);
    });
 
    return pr;
}

Siempre que queramos encadenar tareas, hay que definir cada una de esas tareas en una función.

Primera tarea:

var task1 = function() {
    console.log("Iniciando tarea 1");
    return demo(100);
}

Segunda tarea:

var task2 = function() {
    console.log("Iniciando tarea 2");
    return demo(2000);
}

Para encadenar, primero llamamos a la primera tarea y luego, como la primera tarea devuelve una promise, cuando termine esta, llamaremos a la segunda tarea:

console.log("Inicio");
task1().then(task2).then(function() {
    console.log("Fin de todas las tareas");
    });

Composición de promises

Captura de errores en promises

informatica/programacion/cursos/programacion_avanzada_javascript/promises.1728983593.txt.gz · Última modificación: por tempwin