ES6 nos ha traído no solo nuevas API en Javascript, sino también aproximaciones diferentes a los mismos problemas de siempre. Buen ejemplo de ello es cómo los programadores de Javascript enfrentarán el problema de la programación asíncrona mediante el uso de promesas (ya ampliamente usadas y con grandes librerías como Bluebird) y generadores, dejando de lado el complejo callback. La conjunción de estas dos tecnologías, promesas y generadores, junto con librerías de terceros como CO, Q, suspend y la ya comentada BlueBird, nos van a permitir un código más elegante y fluido. Así, nuestras APIs asíncronas se asemejarán al estilo de programación síncrono, sin perder por ello nada de su potencial.

Ciertamente, en internet hay ya muchos tutoriales que aúnan los conceptos de generador y promesa. Aunque he leído unos cuantos con la mejor predisposición, no fue hasta descubrir el excelente post de Karl Seguin cuando tuve mi propio momento eureka.

Empecemos.

Usaré el mismo ejemplo de un postanterior. En este ejemplo, hay un servicio de mudanzas que realiza una serie de tareas para llevar a cabo el cambio de piso y todas ellas serán funciones asíncronas. Este es el código de nuestro servicios

// moveService.js

let _moveService = function(){  
  return function(){
    let timeSpend = (Math.random() * 5) + 1
    return new Promise( function( resolve, reject ){
      setTimeout( resolve.bind( resolve, timeSpend ), timeSpend * 1000 )
    } );
  }
}

export var empaquetar = _moveService()  
export var desmontar = _moveService()  
export var traslado = _moveService()  
export var montar = _moveService()  
export var colocar = _moveService()

Nuestro moveService no es más que un módulo que expone 5 funciones. Todas ellas son funciones asíncronas que devuelven una promesa. Esta promesa se resuelve automáticamente en un intervalo de entre 1 y 5 segundos. Es un código simple, pero que cumple perfectamente su cometido didáctico.

Por otra parte tenemos el cliente de nuestro módulo. El cliente quiere realizar secuencialmente todos los pasos necesarios para llevar a cabo la mudanza y luego registrar el tiempo total que nos ha llevado mudarnos. Es él quien tiene que lidiar con el hecho de que nuestro servicio solo expone funciones asíncronas--en este caso, promesas. El modo habitual de consumirlo sería:

// promises.js

import * as moveService from "./moveService.js"

let totalTimeSpend = 0;  
moveService.empaquetar()  
  .then( (time) => {
    totalTimeSpend += time;
    return moveService.desmontar()
  } )
  .then( (time) => {
    totalTimeSpend += time;
    return moveService.traslado()
  } )
  .then( (time) => {
    totalTimeSpend += time;
    return moveService.montar()
  } )
  .then( (time) => {
    totalTimeSpend += time;
    return moveService.colocar()
  } )
  .then( (time) => {
    totalTimeSpend += time;
    console.log( `Con promesas he tardado en mudarme ${totalTimeSpend} seg. en mudarme` );
  } )

Como puedes observar, no hay nada especial en esta forma de consumir las promesas. Vamos encadenando una con la siguiente y acumulando el tiempo que va tardando cada una de ellas a un contador general. Al final de la cadena imprimimos el tiempo total acumulado:

Este código está bien y fuciona, pero podemos llegar a hacerlo más expresivo si usamos los generadores. Para ello, vamos a crear una función generadora, dentro de la cual vamos a llamar a nuestras promesas secuencialmente como si fuesen funciones síncronas, solo que precediéndolas de la palabra reservada yield.

function* (){  
  let duracionMudanza = 0;

  duracionMudanza += yield moveService.empaquetar()
  duracionMudanza += yield moveService.desmontar()
  duracionMudanza += yield moveService.traslado()
  duracionMudanza += yield moveService.montar()
  duracionMudanza += yield moveService.colocar()

  return duracionMudanza;
}

Sería genial que con eso fuera suficiente pero, por desgracia, no lo es. Esa función por sí sola no nos sirve para el propósito que buscamos. Necesitamos envolver nuestro generador dentro de otra función, y que sea esta la encargada de llamar a la secuencia de promesas cuando la anterior ya haya sido resuelta. Para este ejemplo en concreto voy a usar CO. El resultado final con todo en su lugar sería:

import co from "co";  
import * as moveService from "./moveService";


co(function* (){  
  let duracionMudanza = 0;

  duracionMudanza += yield moveService.empaquetar()
  duracionMudanza += yield moveService.desmontar()
  duracionMudanza += yield moveService.traslado()
  duracionMudanza += yield moveService.montar()
  duracionMudanza += yield moveService.colocar()

  return duracionMudanza;
}).then( (duracionMudanza) => console.log( `Con generadores he tardado ${duracionMudanza} seg. en mudarme` ) );

Ahora sí obtenemos la salida que podríamos esperar:

Hay varios puntos importantes a resaltar en este código:

  • Lo que devolverá nuestra "promesa" será lo que nosotros pasemos al método resolve de la promesa. Tal y como si fuera el resultado de la ejecución de una función síncrona.

  • Si llamamos a reject en la promesa provocaremos un exception y somos nosotros los responsbles de controlarla. Así que piensa que siempre que llamas a una promesa de esta forma puedes provocar una exception en tu código. Contrólala.

  • CO es una función de mayor orden que recibe como parámetro nuestro generador y devuelve una promesa. Fíjate cómo despues de ejecutar CO le he encadenado un then con el resultado devuelto por nuestro generador.

  • Aunque este ejemplo es muy simple, CO tiene más posibilidades de las aquí mostradas. Pásate por su página y léete la documentación :)

El código usado en este post lo puedes encontrar en Github

Si te ha gustado este post, difunde la palabra. Tampoco dudes en dejar comentarios u observaciones. ¡Gracias! :)

Suscríbete a mi lista de correo

* Campos obligatorios