Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:promises

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Ambos lados, revisión anteriorRevisión previa
Próxima revisión
Revisión previa
informatica:programacion:cursos:programacion_avanzada_javascript:promises [2024/10/14 16:19] – [Creación de promises] tempwininformatica:programacion:cursos:programacion_avanzada_javascript:promises [2024/10/30 15:52] (actual) – [DEMO: Errores en promises] tempwin
Línea 211: Línea 211:
 </code> </code>
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-then-devuelve-promise-01.png |}}
-then devuelve una promise +
-</WRAP>+
  
  
Línea 227: Línea 225:
 </code> </code>
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-then-devuelve-promise-error.png |}}
-then devuelve una promise con error +
-</WRAP>+
  
 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'': 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'':
Línea 249: Línea 245:
 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//": 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//":
  
-<WRAP center round todo 60%> +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-then-devuelve-promise-interna.png |}}
-la promise de then depende de la promise interna +
-</WRAP>+
  
 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. 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.
Línea 342: Línea 336:
 </code> </code>
  
 +===== DEMO: Creación y uso de promises =====
 +
 +Partimos de lo siguiente:
 +
 +<code javascript>
 +var demo = function(res, cb) {
 +    window.setTimeout(function() { cb(res); }, 2000 );
 +}
 +</code>
 +
 +Para invocarla:
 +
 +<code javascript>
 +demo(10, function(r) {
 +    console.log("Recibido ->", r);
 +});
 +</code>
 +
 +Vamos a transformar esa función para utilizar una //promise// y así no tener que usar //callbacks//.
 +
 +<code javascript>
 +var demo = function(res) {
 +    
 +    var pr = new Promise(function(resolve, reject) {
 +        window.setTimeout(function() { resolve(res); }, 2000 );
 +    });
 +    
 +    return pr;
 +}
 +</code>
 +
 +Para utilizarla:
 +
 +<code javascript>
 +var task = demo(10);
 +
 +task.then(function(r) {
 +    console.log("Recibido ->", r);
 +})
 +</code>
 +
 +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.
 +
 +<code javascript>
 +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;
 +}
 +</code>
 +
 +Siempre que queramos encadenar tareas, hay que definir cada una de esas tareas en una función.
 +
 +Primera tarea:
 +
 +<code javascript>
 +var task1 = function() {
 +    console.log("Iniciando tarea 1");
 +    return demo(100);
 +}
 +</code>
 +
 +Segunda tarea:
 +
 +<code javascript>
 +var task2 = function() {
 +    console.log("Iniciando tarea 2");
 +    return demo(2000);
 +}
 +</code>
 +
 +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:
 +
 +<code javascript>
 +console.log("Inicio");
 +task1().then(task2).then(function() {
 +    console.log("Fin de todas las tareas");
 +    });
 +</code>
 +
 +===== DEMO: Encadenar Promises y tratar el resultado =====
 +
 +En el apartado anterior hemos visto cómo encadenar las //promises//, pero no hemos hecho nada con los resultados. Cada una devolvía un resultado. Veamos cómo podemos tratarlos.
 +
 +Primero tenemos que ver cómo la tarea 2 puede saber cuál es el resultado de la tarea 1. Basta con que lo reciba como parámetro:
 +
 +<code javascript>
 +var task2 = function(rp) {
 +    console.log("El resultado previo es ->" , rp);
 +    console.log("Iniciando tarea 2");
 +    return demo(2000);
 +}
 +</code>
 +
 +Para tratar el resultado desde la propia tarea:
 +
 +<code javascript>
 +var task1 = function() {
 +    console.log("Iniciando tarea 1");
 +    return demo(100).then(function(r) {
 +        console.log("En task1() el resultado es ", r);
 +    });
 +}
 +</code>
 +
 +Sin embargo, perdemos el resultado en ''task2'' por haber usado el ''then'' en ''task1''. Modificamos ''task1'' para devolver el resultado:
 +
 +<code javascript>
 +var task1 = function() {
 +    console.log("Iniciando tarea 1");
 +    return demo(100).then(function(r) {
 +        console.log("En task1() el resultado es", r);
 +        return r;
 +    });
 +}
 +</code>
 +
 +Todo el código junto:
 +
 +<code javascript>
 +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;
 +}
 +
 +var task1 = function() {
 +    console.log("Iniciando tarea 1");
 +    return demo(100).then(function(r) {
 +        console.log("En task1() el resultado es", r);
 +        return r;
 +    });
 +}
 +
 +var task2 = function(rp) {
 +    console.log("El resultado previo es ->" , rp);
 +    console.log("Iniciando tarea 2");
 +    return demo(2000);
 +}
 +
 +// Uso:
 +console.log("Inicio");
 +
 +task1().then(task2).then(function() {
 +    console.log("Fin de todas las tareas");
 +    });
 +</code>
 ===== Composición de promises ===== ===== Composición de promises =====
 +
 +Una idea interesante de las promises es que pueden crearse **//promises// compuestas**, que **solo se resuelven cuando todas las //promises// que las componen se resuelven** a su vez (o que entran en el estado de error, en cuanto una de las promises que las componen entra en estado de error).
 +
 +<WRAP center round important 60%>
 +Para esta lección, se supone la existencia de una función llamada ''xhrget'' a la que se le pasa un parámetro (una cadena con URL) y usando ''XMLHttpRequest'' realiza una llamada AJAX GET a la URL indicada y devuelve una promise que se resuelve tan pronto como la petición AJAX está realizada. Se propone como ejercicio el realizar dicha función. En el fichero ''then'' puedes conseguir lo propuesto, con la salvedad de que estás forzando a que las 3 peticiones se ejecuten una tras otra de forma secuencial, cuando no hay necesidad de ello. En estos casos es cuando las //promises// compuestas son extremadamente útiles. Y, si nunca has tenido que resolver escenarios similares sin //promises//, usando variables contadoras y demás, apreciarás todavía más la simplicidad de las //promises// compuestas.
 +</WRAP>
 +
 +==== Promise.all ====
 +
 +Para crear una //promise// compuesta, se usa el método ''Promise.all''. A este método se le pasa un "iterable" (p. ej. un array) de //promises// y nos devuelve la //promise// compuesta. Tan simple como:
 +
 +<code javascript>
 +var p1 = xhrget('http://www.campusmvp.es/');
 +var p2 = xhrget('http://www.campusmvp.es/catalogo/');
 +var p3 = xhrget('http://www.campusmvp.es/opiniones-campusmvp.htm');
 +var pall = Promise.all([p1,p2,p3]);
 +pall.then(function() {
 +    // En este punto las TRES promises (p1, p2 y p3) están resueltas. 
 +});
 +</code>
 +
 +<WRAP center round info 60%>
 +El concepto de "iterable" es nuevo en ECMAScript 2015. Básicamente por ahora podemos considerar que los iterables son arrays, aunque hay otros tipos que también lo son y más adelante veremos cómo hacer que nuestros propios objetos se comporten como iterables si así lo deseamos.
 +</WRAP>
 +
 +
 +Por supuesto queda por aclarar un punto: cómo obtenemos los valores asociados a las distintas //promises// en el ''then'' de la //promise// compuesta.
 +
 +El método ''then'' de una //promise// compuesta **recibe como parámetro un array que contiene todos los resultados de las //promises// que la componen**.
 +
 +En nuestro caso dado que la //promise// devuelta por la función ''xhrget'' tiene como valor el propio objeto ''XMLHttpRequest'' usado, el método ''then'' de la //promise// compuesta recibe como parámetro un array de 3 elementos ''XMLHttpRequest''.
 +
 +El siguiente código ilustra con más claridad este concepto:
 +
 +<code javascript>
 +var p1 = new Promise(function(r,j) { r('one');});   // resolvemos con 'one'
 +var p2 = new Promise(function(r,j) { r(42);});   // resolvemos con 42
 +var p3 = new Promise(function(r,j) { r({foo:'bar'});});   // resolvemos con {foo:'bar'}
 +Promise.all([p1,p2,p3]).then(function(values) { console.log(values)}); // ['one', 42, {foo:'bar'}]
 +</code>
 +
 +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-promise-all.png |}}
 +
 +===== DEMO: Composición de Promises =====
 +
 +Veremos cómo podemos ejecutar //promises// todas a la vez, de forma paralela.
 +
 +Partimos de:
 +
 +<code javascript>
 +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;
 +}
 +
 +var task1 = function() {
 +    console.log("Iniciando tarea 1");
 +    return demo(100).then(function(r) {
 +        console.log("En task1() el resultado es ', r);
 +        return r;
 +    });
 +}
 +
 +var task2 = function(rp) {
 +    console.log("El resultado previo es ->" , rp);
 +    console.log("Iniciando tarea 2");
 +    return demo(2000);
 +}
 +</code>
 +
 +<code javascript>
 +console.log("Inicio");
 +Promise.all([task1(), task2()]).then(function () {
 +    console.log("Fin", arguments[0]);
 +});
 +</code>
 +
 +''arguments[0]'' es un array con los resultados de todas las //promises// que forman la //promise// paralela.
 +
 +<WRAP center round tip 60%>
 +Esto va muy bien para sincronizar peticiones AJAX. Este método nos ahorra utilizar contadores o variables globales.
 +</WRAP>
  
 ===== Captura de errores en promises ===== ===== Captura de errores en promises =====
  
 +Cuando una //promise// entra en estado de error se invoca a su método ''catch''. El método ''catch'' es el equivalente a ''then'', pero se ejecuta cuando la //promise// entra en estado de error en lugar de cuando se resuelve.
 +
 +La función ejecutora de una //promise// utiliza su segundo parámetro (usualmente llamado ''reject'') para pasar la //promise// a estado de error:
 +
 +<code javascript>
 +var errored = new Promise(function(resolve, reject) {
 +    reject(new Error("Some error hapened"));
 +});
 +console.log(errored);
 +</code>
 +
 +En este código la promise ''errored'' está en estado de error. Eso significa que su método ''then'' no se va a ejecutar nunca (dado que no está resuelta ni se va a poder resolver). El método que se va a ejecutar es el método ''catch'':
 +
 +<code javascript>
 +errored.catch(function(err) {
 +    console.log(err);
 +});
 +</code>
 +
 +{{ :informatica:programacion:cursos:programacion_avanzada_javascript:10-promise-catch.png |}}
 +
 +El método ''catch'' comparte todas las otras características del método ''then'':
 +
 +  * Si ejecutas ''catch'' sobre una //promise// que YA está en estado de error, el método se ejecuta al momento.
 +  * La función que se pasa como parámetro a ''catch'' recibe como parámetro el valor que la función ejecutora de la //promise// pasó a ''reject''. (Observa como en el ejemplo, el valor del parámetro ''err'' es el objeto ''Error'' que se ha pasado como parámetro al ''reject'').
 +  * El método ''catch'' devuelve otra //promise// cuyo estado depende de la función que se pase a este ''catch'' (con exactamente las mismas normas que en el caso del método ''then'').
 +
 +===== DEMO: Errores en promises =====
 +
 +Vamos a forzar un error, es decir, que se devuelva una promise en estado de error.
 +
 +<code javascript>
 +var demo = function(res) {
 +    var pr = new Promise(function(resolve, reject) {
 +        if (res < 0) {
 +            // Pasamos la promise a estado de error
 +            reject(new Error("El resultado debe ser mayor que cero"));
 +        }
 +        // Por si se generase algún otro tipo de error, lo capturamos
 +        try {
 +            window.setTimeout(function() { resolve(res)}, 2000);
 +        }
 +        catch (e) {
 +            reject(e);
 +        }
 +    });
 + 
 +    return pr;
 +}
 +</code>
 +
 +Creamos la tarea y capturamos el error:
 +
 +<code javascript>
 +var task = demo(-10); // Forzamos el error
 +
 +// Nunca va ejecutará porque la promise no
 +// pasará a estado resuelto
 +task.then(function(r) {
 +    console.log("Recibido -> ", r);
 +}
 +
 +// Capturamos el error
 +task.catch(function(e) {
 +    console.log("Error recibido -> ", e);
 +});
 +
 +console.log("Fin");
 +</code>
 +
 +===== ¿Cómo limitar el tiempo máximo de ejecución de una Promise? =====
 +
 +Si la API que usamos dentro de la //Promise// ofrece algún mecanismo de //timeout//, lo ideal es usarlo, pero si no es el caso (como curiosamente ocurre con ''fetch'') existe un mecanismo muy sencillo para conseguirlo: usar ''Promise.race''. Esta función toma un iterable de //promises// y devuelva otra promesa que se resuelve/rechaza en cuanto una de las promesas del iterable se resuelva o se rechace.
 +
 +Por lo tanto el mecanismo es muy sencillo. Esta función toma una Promise y devuelve otra con un timeout:
 +
 +<code javascript>
 +const TimeoutPromise = (pr, timeout) =>
 +  Promise.race([pr, new Promise((_, rej) =>
 +    setTimeout(rej, timeout)
 +  )]);
 +</code>
 +
 +Su uso es muy sencillo:
 +
 +<code javascript>
 +let pr = fetch('http://slowwly.robertomurray.co.uk/delay/8000/url/http://www.google.co.uk', {mode: 'no-cors'});
 +let tpr = TimeoutPromise(pr, 500)
 +  .then(() => console.log('fetch done'))
 +  .catch(() =>console.log('timeout cancelled'));
 +</code>
 +
 +La //promise// ''pr'' es una promise que tarda 8 segundos en resolverse (es lo que tarda en cargar esa página), pero si ejecutas ese código verás que al cabo de 500ms la promise ''tpr'' es rechazada.
 +
 +Más fácil imposible, ¿verdad?
 +
 +Bien, nada es perfecto: este método cuando se rechaza la promise por el timeout, **el resto de promises internas siguen ejecutándose**. Es decir, en nuestro caso, tpr es rechazada al cabo de 500ms, pero la promise pr se sigue ejecutando durante los 8s (hasta que se completa la petición de red). Por lo tanto **este mecanismo no aborta promises**. Se limita a envolverlas con una promesa que, esta sí, se resuelve/rechaza en un tiempo máximo.
 +
 +Lo ideal sería poder cancelar la promise, pero ECMAScript no ofrece un mecanismo para poder hacerlo, aunque [[https://github.com/tc39/proposal-cancellation|el TC39 está trabajando en ello]].
 +
 +==== "Cancelando" promises ====
 +
 +Existen [[https://medium.com/@benlesh/promise-cancellation-is-dead-long-live-promise-cancellation-c6601f1f5082|otras técnicas]], pero si para ti es vital el poder "cancelar promises" aquí tienes un código muy sencillo que permite **interrumpir una promise en cada uno de sus pasos**. Cada uno de esos pasos debe ser a su vez una promise:
 +
 +<code javascript>
 +const token = function() { 
 +  return {
 +    cancel: function () {this.isCancelled = true},
 +    isCancelled: false
 +    }
 +}
 +async function  MultistepPromise (iterf, token)
 +{
 +  for (let f of iterf) {
 +    await f();
 +    if (token.isCancelled)  {
 +      throw 'promise cancelled';
 +    }
 +  }
 +}
 +</code>
 +
 +Bueno, como puedes ver el código es trivial: simplemente itera por el iterable de promises si por alguna razón el token se cancela.
 +
 +Su uso es, de nuevo, muy sencillo:
 +
 +<code javascript>
 +const func = () => fetch(
 +  'http://slowwly.robertomurray.co.uk/delay/3000/url/http://www.google.co.uk',  
 +  {mode: 'no-cors'});
 +const tok = token();
 +const pr = MultistepPromise([func, func, func, func, func, func], tok)
 +  .then(() => console.log('all acceoted'))
 +  .catch((e) => console.log('some error', e));
 +</code>
 +
 +Si una vez ''pr'' se está ejecutando, llamas a ''tok.cancel()'', entonces ''pr'' se cancelará una vez se haya finalizado el paso correspondiente (a medio paso no se puede cancelar).
 +
 +Observa que ''MultistepPromise'' no espera un array de promises, sino simplemente un iterable de promises, por lo que puedes usar otras técnicas tales como un generador para pasarle las promises a ejecutar.
 +
 +Por supuesto, puedes combinar ambas técnicas: es decir, hacer una ''MultistepPromise'', donde cada uno de sus pasos sea ''TimeoutPromise''. De este modo te aseguras que si uno de los pasos excede el tiempo, todo el resto de pasos se cancelan (no se ejecutan).
 +===== Recursos =====
  
 +  * [[https://promisesaplus.com/|Especificación oficial de Promises/A+]]
 +  * [[https://github.com/stefanpenner/es6-promise|Polyfill de Promises/A+]]
 +  * [[https://dmitripavlutin.com/javascript-promises-then-vs-then-catch/|JavaScript Promises: then(f,f) vs then(f).catch(f)]]
informatica/programacion/cursos/programacion_avanzada_javascript/promises.1728915593.txt.gz · Última modificación: por tempwin