La programación asíncrona es una de las grandes ventajas de Javascript y uno de los grandes secretos de la gestión de la concurrencia de Node.js, pero también es fuente de problemas, errores y confusiones, tanto para los programadores que están empezando, como para los más expertos.
Utilizar la programación asíncrona para gestionar eventos ya puede llegar a ser algo complicado de comprender, pero cuando invocamos a una función o método y este nos pide un callback o nos devuelve una promesa para gestionar la respuesta de forma asíncrona, entonces es cuando realmente se nos empieza a complicar nuestra comprensión del código y su flujo de ejecución.
Parece que a todos nos cuesta comprender el funcionamiento asíncrono del código, es fácil confundirse, perderse y no comprender que camino ha tomado la ejecución. Por ello es necesario que tomemos el control de la ejecución asíncrona.
Vamos a ver varios modelos de control cuando tenemos que lanzar un conjunto de funciones asíncronas y necesitamos asegurarnos de que se ejecutan todas las órdenes. También veremos cómo podemos conseguir que se ejecuten una orden detrás de otra de forma secuencial. Todo ello con cada una de las técnicas que tenemos a nuestro alcance: callbacks, promesas, generadoras, co()
, async
/ await
.
Si eres de los que prefieres ver y escuchar un vídeo en vez de leer un texto, también tienes la posibilidad de ver este vídeo de la sesión de Madrid JS donde se explica de forma dinámica los diferentes ejemplos que puedes encontrar más abajo.
Función asíncrona de ejemplo
Primero vamos a definir una sencilla función asíncrona que nos servirá para mostrar las diferentes aproximaciones sobre el control. No es una función útil, simplemente calcula el cuadrado de un número de forma asíncrona y además retrasa de forma aleatoria su ejecución por medio de setTimeout() y Math.random().
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } asyncSqrt(2, function(value, result) { console.log('END execution with value =', value, 'and result =', result); }); console.log('COMPLETED ?');
Como se puede observar hemos incluido un mensaje por consola para ver cuándo se ha realizado la llamada a este código y otro mensaje por consola para comprobar el valor retornado.
Callbacks
Ejecución asíncrona natural
En el siguiente código vamos a realizar la ejecución asíncrona pasando valores del 0 al 9 a nuestra función. No vamos a utilizar ningún tipo de mecanismo de control de la ejecución y podremos comprobar lo que sería una ejecución asíncrona natural.
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } for (var n = 0; n < 10; n++) { asyncSqrt(n, function(value, result) { console.log('END execution with value =', value, 'and result =', result); }); } console.log('COMPLETED ?');
El resultado de ejecutar este código puede desconcertar un poco, ya que si fuera un código síncrono esperaríamos ver los mensajes STAR y END uno detrás de otro y terminados por COMPLETED, pero al ser un código asíncrono el resultado es completamente diferente.
Lo que ha pasado es que se ha ejecutado el bucle for
con cada una de las llamadas a asynSqrt()
y directamente se ha pasado a ejecutar el mensaje COMPLETED, ya que todas las llamadas a nuestra función se ejecutan de forma asíncrona. Los resultados de esta ejecución no siguen ningún orden, ya que cada una de estas llamadas concluye sin respetar el orden de llamada.
Controlar que se ha ejecutado todo
Para controlar que todas las llamadas se han ejecutado podemos utilizar un sencillo mecanismo basado en dos variables, una con el valor de ejecuciones que tenemos que realizar y otro con un contador. Este mecanismo no es muy sofisticado, pero funciona muy bien para controlar la ejecución de todas las órdenes que hemos lanzado.
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } var max = 10; var cnt = 0; for (var l = 0; l < max; l++) { asyncSqrt(l, function(value, result) { console.log('END execution with value =', value, 'and result =', result); if (++cnt === max) { console.log('COMPLETED'); } }); }
Con este sencillo mecanismo el mensaje COMPLETED se muestra sólo cuando se hayan ejecutado todas las llamadas a la función asíncrona.
Ejecución secuencial
En ocasiones esto no es suficiente y querremos que cada una de las llamadas se resuelva de forma secuencial a la anterior, por ejemplo: por qué necesitamos el resultado de la llamada anterior dentro de la llamada siguiente. La primera aproximación a este problema es encadenar las llamadas, es decir, llamar a la siguiente función en el callback de la llamada anterior.
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function () { callback(value, value * value); }, 0 | Math.random() * 100); } asyncSqrt(0, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(1, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(2, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(3, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(4, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(5, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(6, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(7, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(8, function (value, result) { console.log('END execution with value =', value, 'and result =', result); asyncSqrt(9, function (value, result) { console.log('END execution with value =', value, 'and result =', result); console.log('COMPLETED'); }); }); }); }); }); }); }); }); }); });
Este tipo de estructura del cógido se ha denominado callbacks hell o el infierno de los callbacks, ya que las funciones se van encadenando de forma que la indentanción del código se vuelve bastante prominente y dificulta la comprensión del código.
Para resolver este problema podemos encadenar las llamadas de una función con la siguiente por medio de este modelo donde se va llamado sucesivamente a la función, no siempre se podrá utilizar esta estrategia, pero es una solución en algunos casos:
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } var max = 10; var cnt = 0; asyncSqrt(cnt, function callback(value, result) { console.log('END execution with value =', value, 'and result =', result); if (++cnt === max) { console.log('COMPLETED'); } else { asyncSqrt(cnt, callback); } });
De esta forma conseguimos que la ejecución se realice una llamada tras otra. Lo que hemos hecho es que en la función callback se llama a asynSqrt()
y esta a su vez llama al callback, por lo tanto se va llamando de forma secuencial hasta que se completan las llamadas. Este es un ejemplo muy sencillo y los casos reales pueden llegar a ser mucho más complejos, pero el modelo es válido para secuenciar las llamadas asíncronas.
Cualquiera de estos modelos de ejecución secuencial produce un aumento significativo del tiempo total de proceso, ya que se debe esperar a terminar una llamada para lanzar la siguiente, pero hemos conseguido realizar una ejecución secuencial, que es lo que estábamos buscando.
Controlar todo con una función
Existe una buena colección de librerías y frameworks que ofrecen funciones muy útiles para gestionar estas circunstancias de forma sencilla. Nacho Ariza nos comentaba async-foreach, async.parallel() y async.waterfall.
Si queremos hacerlo por nosotros mismos no es un gran esfuerzo. Este es un ejemplo de implementación breve y concisa para ejecutar repetidas veces una función de forma secuencial o paralela (depende del true/false del último parámetro) sobre un conjunto de valores:
"use strict"; function forEachAll(data, each, finish, sync) { var n = -1, result = []; var next = sync ? function () { if (++n < data.length) { each(data[n], result, next); } else if (finish) { finish(result); } } : (function () { function completed() { if (++n <= data.length && finish) { finish(result); } } for (var i = 0; i < data.length; i++) { each(data[i], result, completed); } return completed; }()); next(); } function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } forEachAll([0,1,2,3,4,5,6,7,8,9], function(value, allresult, next) { asyncSqrt(value, function(value, result) { console.log('END execution with value =', value, 'and result =', result); allresult.push({value: value, result: result}); next(); }); }, function(allresult) { console.log('COMPLETED'); console.log(allresult); }, true );
El uso de forEachAll()
es bastante sencillo:
- El primer parámetro es una matriz con todos los valores que queremos procesar.
- El segundo parámetro es la función que realiza la ejecución para cada llamada. Esta función recibe el valor que le corresponde, una matriz para guardar los resultados y una función
next()
para llamar cuando se haya completado el paso. - El tercer parámetro es la función que se ejecutará al finalizar la ejecución completa y recibe en una matriz el resultado de todas las llamadas.
- Por último, el cuatro parámetro indica si la llamada se hace de forma secuencial (
true
) o de forma paralela (false
). Prueba a cambiar este último parámetro y vuelve a ejecutar el ejemplo. Verás que el resultado es completamente diferente.
Durante bastante tiempo hemos utilizado hemos utilizado forEachAll()
en sistemas en producción con resultados muy buenos, por lo que es una buena alternativa para gestionar la asincronía en los casos donde los datos de entrada pueden ser matrices de valores.
Promesas
Una de las soluciones más interesantes para controlar la ejecución asíncrona son las promesas. Afortunadamente ya son parte del estándar y tiene soporte nativo desde Node.js 0.12, Chrome 32, Firefox 27, Edge 12, Safari 7.1 y Opera 19. También existen muy buenas librerías que ofrecen promesas en entornos donde no están disponibles de forma nativa.
Lo primero es modificar nuestra función para que en vez de utilizar un callback utilice promesas para responder de su ejecución. Como veremos más adelante podemos utilizar funciones auxiliares para realizar esta conversión, pero, dada la sencillez de nuestro ejemplo, vamos a transformar la función para devolver una promesa.
"use strict"; function promiseSqrt(value){ console.log('START execution with value =', value); return new Promise(function (fulfill, reject){ setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } promiseSqrt(2).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); }); console.log('COMPLETED ?');
Ahora la función devuelve una promesa en vez de recibir un callback. Esta promesa pasa dos funciones al cuerpo de la función, uno para indicar que todo ha ido bien y otra para indicar que se ha producir un error. En este sencillo ejemplo sólo vamos a utilizar la función de éxito.
Como fullfill()
sólo acepta un parámetro, se han agrupado los dos valores que antes se pasaban al callback dentro de un objeto y este se ha pasado como parámetro. Con la asignación por destructuring de ES6 podemos recibir el objeto como si fueran varios parámetros, pero esa característica la explicaremos en otro momento.
Promise.all() para controlar la ejecución de todas las promesas
Ahora podemos hacer uso de Promise.all() para controlar que todas las llamadas han terminado y recibiremos en una matriz todos los resultados.
"use strict"; function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } var p = []; for (var n = 0; n < 10; n++) { p.push(promiseSqrt(n, n * 2)); } Promise.all(p).then(function(results) { results.forEach(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); }); console.log('COMPLENTED'); });
Un detalle importante: aunque la matriz que recibimos está ordenada de igual forma que las llamadas que hemos realizado, la ejecución de cada una de las funciones promiseSqrt()
se ha realizado sin esperar a la anterior y es Promise.all()
quien se ha encargado de ordenar los resultados.
Ejecución secuencial con promesas
El ejemplo anterior no resuelve por si sólo la ejecución secuencial de las llamadas a las distintas funciones, por lo que tenemos que utilizar un modelo algo diferente para que se comporte de esta forma.
La primera opción es encadenar las llamadas a las promesas. Es algo que cada día se ve con más asiduidad: una lista de .then()
encadenados. Aunque en nuestro caso carece de sentido, vamos a ver como sería:
"use strict"; function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } promiseSqrt(0).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(1); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(2); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(3); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(4); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(5); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(6); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(7); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(8); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); return promiseSqrt(9); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); }).catch(function(err) { console.error(err); });
Aunque este modelo funciona, la verdad es que tampoco es muy cómodo de seguir, además de estar limitado al conocimiento previo de los elementos que debemos encadenar. En nuestro ejemplo, deberíamos incluir nuevas líneas de código si queremos incluir más valores a analizar. No obstante es un tipo de código que funciona correctamente y seguramente es preferible al callback hell.
Algo interesante y que podemos aprovechar es que el retorno del callback pasado al método then()
se convertirá en una promesa, aunque originalmente no lo sea, por lo que podemos encadenar llamadas con la confianza de que siempre se devolverá una promesa.
Por otra parte, existen soluciones como promise-sequential para gestionar este tipo de situaciones. Pero, nuevamente. no es muy complicado implementar por nosotros mismos este tipo de ejecución por medio de un código de este tipo:
"use strict"; function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } var p = [0,1,2,3,4,5,6,7,8,9]; p.reduce( function (sequence, value) { return sequence.then(function() { return promiseSqrt(value); }).then(function(obj) { console.log('END execution with value =', obj.value, 'and result =', obj.result); }); }, Promise.resolve() ).then(function() { console.log('COMPLETED'); });
Vamos a explicar paso a paso lo que hacemos en este caso:
1) En primer lugar se ha creado una matriz con todos los valores que queremos procesar y hemos llamado al método reduce() que tiene dos parámetros:
- el primero es una función que será llamada para cada uno de los elementos de la matriz y que recibe cuatro parámetros, aunque en este caso sólo vamos a utilizar los dos primeros, que corresponden al valor previo y al valor actual de la matriz.
- el segundo es opcional y corresponde al valor que se pasará a la función al iniciar el recorrido de la matriz.
2) Como valor inicial de la función reduce() estamos pasando Promise.resolve() que es una promesa que damos ya por correcta. Esto es así para que la primera llamada a secuence.then() se ejecute correctamente. En la siguientes llamadas sequence corresponde a la promesa anterior que es devuelta por promiseSqrt().
3) Cada una de las llamadas a promiseSqrt() devuelve una promesa, que como hemos dicho, es pasada como valor anterior a la función del reduce() y por lo tanto no se ejecuta hasta que no se ha terminado la ejecución anterior.
4) Para finalizar hay otra instrucción .then() que se ejecuta cuando se han concluido todas las ejecuciones anteriores.
Puede parecer un poco enrevesado, pero una vez que se conoce sólo hay que copiar este modelo y aplicarlo en cada una de las situaciones que nos podemos encontrar en los que queremos ejecutar una misma promesa sobre diferentes valores.
Ejecución con generadores
Otra aproximación es utilizar funciones generadoras. Este tipo de funciones son una potente funcionalidad de ES6 y están disponibles de forma nativa en Node.js 4.x, Chrome 39, Firefox 26.0, Edge 13, Safari 10 y Opera 26. Para hacer una llamada secuencial con generadores deberemos llamar a la función next() dentro del callback, tal y como nos han indicado Javier Miguel y Javier Abadía. El código utiliza asyncSqrt() que definimos más arriba y es este:
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } function* gen(callback) { for (var i = 0; i < 10; i++) { yield asyncSqrt(i, callback); } } var iterator = gen(function (value, result) { console.log('END execution with value =', value, 'and result =', result); iterator.next(); }); iterator.next();
Como se puede ver el resultado es bastante manejable. Básicamente se utiliza una función generadora, se instancia y se hacen llamadas a next()
, una fuera de la función para iniciar el proceso y el resto están dentro de la función que es pasada como callback.
Un error común: usar yield dentro de un callback definido dentro de la función generadora
Algo que resulta poco intuitivo es que yield
solo se puede incluir dentro de una función generadora. En ocasiones podemos insertar en nuestra función generadora un callback
, por ejemplo, en un forEach()
, pero esa función no es una generadora y no podremos incluir ahí una instrucción yield
. Por ejemplo, este código no funciona ya que yield
no está realmente dentro de una función generadora:
"use strict"; function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(value, value * value); }, 0 | Math.random() * 100); } function* gen(callback) { var arr = [0,1,2,3,4,5,6,7,8,9]; arr.forEach(function (i) { yield asyncSqrt(i, callback); }); } var iterator = gen(function (value, result) { console.log('END execution with value =', value, 'and result =', result); iterator.next(); }); iterator.next();
En las funciones generadores se suele recorrer las matrices por medio de instrucciones for
, for in
o for of
, evitando el uso de los métodos de gestión de las matrices si vamos a utilizar instrucciones yield
para cada uno de los elementos de la matriz.
Complementar la funciones generadoras con co()
Aunque el modelo basado sólo en una función generadora funciona, lo cierto es que tenemos que encargarnos de llamar sucesivamente a next()
y hace que el sistema se complique un poco. Para solucionar esta complicación y hacernos las vida más sencilla tenemos un poderoso aliado: co.
Antes de adentrarnos en co()
vamos a ver un implementación mínima de este tipo de corrutinas y que hemos denominado myCo()
. Como se puede observar lo que hacemos es llamar next()
sucesivamente y obteniendo las promesas y obteniendo el resultado y volviendo a llamar al siguiente next()
.
"use strict"; function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } function myCo(gen) { var i = gen(); function sequent(result) { var ret = i.next(result); if (!ret.done) { ret.value.then(sequent); } } sequent(); } myCo(function* gen() { for (var n = 0; n <= 9; n++) { var obj = yield promiseSqrt(n); console.log('END execution with value =', obj.value, 'and result =', obj.result); } });
Aunque pueda parecer que estamos haciendo algo raro o misterioso, lo cierto es bastante sencillo de comprender. El secreto está en que en un código del tipo var obj = yield promiseSqrt(n);
el valor que se incluye en obj
es realmente el valor que se devuelve en el siguiente next()
y si esa llamada no tienen ningún valor, entonces se incluye el valor devuelto por promiseSqrt(n)
. Es decir, al llamar al anterior next()
se obtiene una promesa y cuando esta es resuelta, se llama al siguiente next()
incluyendo su resultado como parámetro para que sea utilizado como asignación en el valor a la izquierda del yield
.
Nuestro pequeño ejemplo es funcional, pero no gestiona los errores, no es capaz de trabajar con matrices de promesas u otros tipos de datos y desde luego no es un código muy probado. Para realizar una gestión completa podemos utilizar co()
, una popular librería que nos permite controlar la ejecución asíncrona de forma muy sencilla, lo cual hace que nuestro código sea realmente compacto y fácil de leer. Vamos a ver nuestro ejemplo en NodeJS con la librería co
:
"use strict"; const co = require('co'); function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } co(function* () { for (var n = 0; n <= 9; n++) { var obj = yield promiseSqrt(n); console.log('END execution with value =', obj.value, 'and result =', obj.result); } });
Esta librería co()
no sólo funciona con promesas, si no que también puede trabajar con otros elementos donde ubiquemos antes un yield
:
- Promesas
- thunks (funciones)
- array (ejecución en paralelo de los elementos)
- objectos (ejecución en paralelo de los miembros)
- generadores (delegación)
- funciones generadoras (delegación)
En ocasiones es posible que no podamos utilizar un bucle para hacer las llamadas a las diferentes funciones asíncronas y tengamos que situar una detrás de la otra para hacer todo lo que necesitamos. En este caso el código con co()
queda bastante claro de leer:
"use strict"; const co = require('co'); function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } co(function* () { var obj = yield promiseSqrt(0); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(1); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(2); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(3); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(4); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(5); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(6); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(7); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(8); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(9); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = yield promiseSqrt(10); console.log('END execution with value =', obj.value, 'and result =', obj.result); });
Aunque nuestro ejemplo no tiene mucha utilidad, podemos ver como podemos situar una tras otras las instrucciones yield
siguiendo un orden de ejecución que nos recuerda mucho a la ejecución síncrona, aunque realmente se esté realizando una ejecución asíncrona cada vez que aparece un yield
.
Thunks y co()
Especialmente interesante es el uso de co()
con los thunk, es decir, una función que permite el paso de un modelo o framework a otro. En nuestro caso el thunk se utiliza para pasar de un modelo basado en callbacks a un modelo que pueda ser utilizado por co()
en una instrucción yield
. Para ello tenemos que hacer un par de cosas.
1) En primer lugar nuestro callback debe utilizar el patrón típico de NodeJS: callback(err, value1, [value2], ...)
, es decir, el primer parámetro debe ser el código de error o null
, seguido del resto de valores. Esta convención está muy extendida y no será un problema, aunque en nuestro pequeño ejemplo deberemos cambiar la forma de llamar al callback, ya que no gestionamos ningún tipo de error.
2) En segundo lugar debemos crear una función que realice una ejecución parcial con los parámetros de entrada y devuelva una función que sólo reciba el callback. Podemos usar una librería como thunkify, pero como venimos haciendo, lo haremos nosotros mismos para entender su funcionamiento:
"use strict"; const co = require('co'); function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(null, {value, result: value * value}); }, 0 | Math.random() * 100); } const thunkAsyncSqrt = function(value) { return function(callback) { asyncSqrt(value, callback); }; }; co(function* () { for (var n = 0; n <= 9; n++) { var obj = yield thunkAsyncSqrt(n); console.log('END execution with value =', obj.value, 'and result =', obj.result); } });
Por último es importante reseñar que co()
devuelve una promesa y esto es de bastante utilidad, ya que podemos anidar las llamadas, gestionar los errores que se puedan producir en un catch()
u obtener el resultado final en un then()
.
Si quieres consultar ejemplos más completos del uso de co()
te recomendamos que leas Acceder a MongoDB con Ecmascript 6 desde NodeJS y Test de un API REST con mocha, chai, co y fetch.
El futuro: async y await
Siguiendo el modelo de co()
para controlar la ejecución asíncrona en Javascript, en breve dispondremos de las instrucciones async y await. Han sido propuestas por Brian Terlson para ser parte del estándar ECMAScript y que se encuentran en un avanzado estado de desarrollo en muchos entornos (de momento están disponibles de forma nativa en Edge 13 con un flag y Node-Chakracore, aunque podemos empezar a comprobar su funcionamiento con Babel).
"use strict"; function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } async function run() { for (var n = 0; n <= 9; n++) { var obj = await promiseSqrt(n); console.log('END execution with value =', obj.value, 'and result =', obj.result); } } run();
La verdad es que se parece mucho al uso de co()
, aunque con algunas diferencias:
- En vez de
co(function* ...
utilizaremosasync function...
- En vez de
yield
utilizaremosawait
- Tendremos que iniciar la ejecución de la función asíncrona, ya que
co()
la ejecuta directamente y en este caso tendremos que hacerlo nosotros, aunque realmente si quieremos el mismo comportamiento podemos usarco.wrap()
.
Si utilizamos varias instrucciones await
segundas unas de otras, nuevamente obtenemos un código con apariencia secuencia y que nos resulta sencillo de leer y comprender, aunque la ejecución de las funciones sea asíncrona.
"use strict"; function promiseSqrt(value){ return new Promise(function (fulfill, reject){ console.log('START execution with value =', value); setTimeout(function() { fulfill({ value: value, result: value * value }); }, 0 | Math.random() * 100); }); } (async function () { var obj = await promiseSqrt(0); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(1); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(2); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(3); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(4); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(5); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(6); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(7); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(8); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(9); console.log('END execution with value =', obj.value, 'and result =', obj.result); obj = await promiseSqrt(10); console.log('END execution with value =', obj.value, 'and result =', obj.result); })();
async
devuelve una promesa, por lo que podemos anidar las llamadas a funciones asíncronas, además de gestionar los errores que se puedan producir en un catch()
u obtener el resultado final en un then()
.
async
/await
se pueden utilizar con funciones generales y funciones flecha, en breve será parte del lenguaje y no requieren ningún tipo de librería externa, por lo que son un futuro muy interesantes.
Convertir callbacks en promesas
Parece ser que await
no funciona con un thunk del estilo que utilizamos en co()
, pero tenemos una alternativa sencilla, convertir las funciones que utilizan callback en funciones que devuelven promesas. Para ello podemos utilizar una librería como pify que convierte en promesas cualquier función que utilice un callback con la convención tipo NodeJS.
Veamos nuestro ejemplo convirtiendo la función basada en callback a promesas con pify()
.
"use strict"; const pify = require('pify'); function asyncSqrt(value, callback) { console.log('START execution with value =', value); setTimeout(function() { callback(null, {value: value, result: value * value}); }, 0 | Math.random() * 100); } const promiseSqrt = pify(asyncSqrt); (async function () { for (var n = 0; n <= 9; n++) { var obj = await promiseSqrt(n); console.log('END execution with value =', obj.value, 'and result =', obj.result); } })();
Las promesas son la base para la gestión con funciones generadores y con async
/ await
, por lo que disponer de una forma sencilla de convertir las tradicionales funciones que utilizan callbacks
Conclusiones
Desde las llamadas asíncronas naturales y los mecanismos que podemos utilizar para controlar su ejecución, pasando por las promesas, las funciones generadoras, co()
, hasta llegar a async
/await
hemos realizado un largo recorrido, muestra inequívoca de lo complejo y difícil que es la gestión de la ejecución asíncrona en Javascript.
Claramente el uso de async
/await
es el mecanismo más claro y fácil de comprender, pero todavía no está disponible, por lo que, mientras se generaliza su implementación, podemos hacer uso de co()
y las funciones generadoras con yield
, ya que su estructura y funcionamiento es muy similar y nos va preparando para el futuro del control de a la ejecución asíncrona en Javascript.
Esperamos que todo este contenido sea de utilidad.
Novedades
HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)
En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.
En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.
Observables en Javascript con Proxies
En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.
Aplicaciones JAMStack, SEO friendly y escalables con NextJS
En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.
Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera
En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.
Gracias por la mención! buen artículo!
Es lo menos que se puede hacer. Realmente la mayoría de lo que aquí se recoge proviene de las fantásticas aportaciones en las listas de distribución de MadridJS y Node.JS Madrid en Meetup.
Hola buenas tardes, quisiera agradecerte el que hayas publicado un valioso vídeo sobre funciones asíncronas y no es por demás, pero le diste al clavo cuando buscaba una solución para obtener los resultados en orden.
Estoy iniciando con Nodejs/Mongo, talvez la pregunta es muy tonta o básica, pero quisiera saber si me puedes ayudar, mi intención es realizar una función y regresar una colección para ser usada en otra función, lo he intentado y no logro dar con la solución.
De antemano te agradezco tu tiempo.
Hola Jesús,
Muchas gracias por tus palabras. Vamos a revisar tu caso paso a paso:
1.- No es necesario setTimeout: como las llamadas a las funciones del driver de MongoDB son asíncronas no es necesario incluir un setTimeout() en el códig.
2.- No es necesario utilizar Promise.all(): basta añadir el then() a la llamada de ObtenerDatos().
3.- Hay una pequeña confusión en el código entre el fullfill y los callbacks.
Hemos cambiado estos temas y nos queda un código de este tipo
Buenísimo el post!
Excelente post.muchas gracias. sugerencia: arreglen ese navbar, haganle fade al hacer scroll o algo de opacity, tapa mucho la visibilidad del contenido del post.
Buena sugerencia. Hemos reducido el tamaño del navbar. Muchas gracias.
hola , estoy trabajando en una api con google maps y marcadores dinamicos, traidos desde firebase, por algun motivo cuando hago el ciclo for dentro de los marcadores, las variables publicas no las toma en cuenta, y cuando comienza a iterar se convierten en locales, luego pierde el valor. ¿existe alguna manera que recomendarias para retornar el valores de las iteraciones en otra funcion de manera que pueda jugar con esos datos?
Creo que te entiendo, pero no estoy seguro. Vamos a comprobarlo.
En este código se muestra 10 veces 10 por consola:
Para solucionar este problema, lo más sencillo es crear una función a la que pasamos como parámetro x, de esta forma se produce una retención del valor para cada iteración del bucle for. Quedaría algo de este tipo
Espero haber acertado con al respuesta a tu problema
Una charla muy interesante y necesaria!
Buenos días,
gracias por tu poderoso artículo que me ha aclarado más de un concepto. Yo llevo muchos años en programación secuencial y esto se me atraviesa un poco.
Estoy intentando llevar a cabo alguno de los puntos que explicas y no consigo hacer lo que pretendo.
Tengo dos tablas mysql, en la primera extraigo x resultados que serán el filtro por el que haré el acceso en la segunda tabla. El resultado será la concatenación de todos los recorset que salgan de la segunda tabla.
Con los datos en la memoria pretendo hacer un mapa svg, así que la lectura de las tablas debería devolver un array con todos las filas. Los dos módulos que leen las tablas funcionan perfectamente, los puedes obviar, es en la organización del main donde me pierdo
Gracias de antemano por tu tiempo, y un saludo
Este seria el main
«use strict»;
var gn= require(‘./getNodos.js’);
var gv= require(‘./getVias.js’);
var mysql = require(‘mysql’);
var conn = require(‘./dbconfig.js’);
var mapaVirtual=[]
function fCB(error,values, rowNodos) {
if(error) {
console.log(error)
conn.end()
return
}
if(rowNodos.length >0 ){
mapaVirtual.push(rowNodos)
console.log (mapaVirtual)
} else {
console.log(‘No data’)
}
}
conn.connect()
gv.getVias(null, function(err, rowVias) {
if (err) {
console.log(err)
return
}
for (var i = 0; i < rowVias.length; i++) {
var values = []
values[0]=rowVias[i].vias_co_vias
values[1]=rowVias[i].vias_co_sentido
gn.getNodos(null,values, fCB)
}
})
//conn.end()
módulo getnodos.js
var mysql = require('mysql');
var conn = require('./dbconfig.js');
var getVias = require('./getVias.js');
function getNodos(err,values, getNodosCallback) {
var selectQNodos = "select nodo_no_nodo, coordenada_x, coordenada_y, nodo_nu_orden, colo_va_hex, vias_co_sentido\
from tmcoormo coor,\
tmnodomo nodo,\
tmviasmo vias,\
tmcoloao colo,\
tmsentao sent\
where nodo.nodo_id_nodo=coor.nodo_id_nodo\
and nodo.vias_id_vias=vias.vias_id_vias\
and vias.colo_id_color=colo.colo_id_color\
and vias.sent_id_sent=sent.sent_id_sentido\
and vias.vias_co_vias=?\
and sent.vias_co_sentido=?";
// connection.connect();
let orderA =" order by nodo_nu_orden asc"
let orderB =" order by nodo_nu_orden desc"
let selectQuNodos =selectQNodos
if (values[1]==='A'){
selectQuNodos=selectQuNodos +orderA
}
else if (values[1]==='B') {
selectQuNodos=selectQuNodos +orderB
}
conn.query(selectQuNodos,values,getNodosCallback)
}
module.exports.getNodos = getNodos;
módulo getVias
var mysql = require('mysql');
var conn = require('./dbconfig.js');
var selectQVias = " select vias.vias_co_vias ,sent.vias_co_sentido from tmviasmo vias,tmsentao sent\
where vias.sent_id_sent=sent.sent_id_sentido";
function getVias(err, getViasCallback) {
if (err) {
getViasCallback(err);
return;
}else{
console.log('Conectado a la base de datos');
}
conn.query(selectQVias, function(err, rowsVias, fields) {
if (err) {
console.log('Error de query');
getViasCallback(err);
return;
}
getViasCallback(err, rowsVias);
});
}
module.exports.getVias = getVias;
El problema es que el flujo sigue antes de que acabe el que se supone que es el callback y el array mapaVirtual está vacio
Muchas gracias por el valioso aporte, Pablo. Lo guardo, como si fuera de oro, entre mis favoritos, ya que es el post más completo explicando la asincronía, pasito a pasito y con ejemplos. Por fin he terminado de entender algunos conceptos.
Hola, buenas, en la primera parte donde explicas Promise.all(), en el código has realizado lo siguiente
p.push(promiseSqrt(n, n * 2));
ese ( n * 2 ), es para despistar o tiene algún propósito
gracias.
Es sólo para despistar. Simplemente quería que no fuera siempre el mismo código, pero entiendo que pueda confundir.
Excelente ponencia relacionada, vaya que me ha ayudado a entender un poco mejor este tema de la asincronía. Tengo una pregunta: haz realizado alguna implementación con para importar datos a Dynamo AWS?
Hola, Pablo.
Le agradezco el artículo, ingenioso y muy bien explicado. He aprendido bastante y he llegado a la solución de mi problema.
Muchísimas gracias y un cordial saludo.
Hola soy novato con js pero queria hacerles una pregunta.
se puede porcesar una consulta por post a php con promesas y como hago para retornar el valor especifico
digamos si la operacion se concreto el response de la solicitud sera 1 en caso contrario 0 y si hubo un error response sera el error; ahora todo esto lo tengo ya hecho, sin embago como es una actualizacion en lotes he tratado de hacerlo con un cliclo de 100 en 100 son alredor de 1600 registros ahora si hago un ciclo comun se envian las 16 consultas al mismo tiempo, lo que quiero es controlar que una consulta se envie a la vez y no se ejecute la proxima consulta hasta obtener respuesta de la anterior, para evitar que el servidor de bd se sature con tantas consultas dado que el volumen de datos a actualizar es grande y afecta 8 tablas.
Excelente articulo y bastante detallado lo he puesto dentro de mi barra de favoritos como información de consulta.
Tengo una consulta al realizar una función que utiliza promise puedo retornar el valor y almacenarlo en una variable? para ser utilizado en otra función. Estoy obteniendo datos de una base de datos en postgres utilizando node-postgres. la función trabaja correctamente y el valor se ve reflejado cuando utilizo el console.log(). pero al utilizar el return me arroja promise {}. Este es mi codigo:
function get_roleRBAC(username)
{
const text = ‘SELECT role_name ‘ +
‘ FROM Users AS U, Roles AS R, user_roles AS UR’ +
‘ WHERE U.username = $1’ +
‘ AND U.userID = UR.userID’ +
‘ AND R.roleID = UR.roleID’;
const values = [username];
let rolename = new Promise(function (resolve, reject)
{
dbcon.query(text, values, function (err, result)
{
if (err) return reject(err.stack);
dbcon.end();
resolve(result.rows)
})
})
return rolename;
}
// llamando a la función
var role = get_roleRBAC(«fulanito»).then(function (results) {
console.log(results[0].role_name); // este funciona
return results[0].role_name; // este tengo dudas si funciona
})
// imprimiendo por consola la variable role
console.log(‘probando la var role:’, role)
// resultado de consola
probando la va role: Promise {}
// resultado de la llamada a la función
Admin
la idea es es utilizar la variable role en otra función. Entiendo que las promesas no se ejecutan de inmediato. Debo implementar un setTimeout? Me podria indicar que estoy haciendo mal o como puedo alcanzar el resultado que busco.
Muchas gracias
Más o menos ya te has respondido directamente.
La variable
role
es siempre la promesa devuelta porget_roleRBAC()
, no contienen el valor pasado a la funciónresolve
, que se recibe siempre en la función que se pasa athen()
.La única forma de que este código se acerque a lo que quieres, es utilizando
async
yawait
. Más o menos podría ser de esta forma:// llamando a la función
(async function () {
var role = await get_roleRBAC(“fulanito”);
console.log(role); // este funciona
})();
Quiero agradecerte de gran manera, de verdad muchas gracias por este aporte! Me salvaste muchísimo la vida al querer ejecutar una query con mongoose dentro de un for, llevaba un día completo buscando información y no encontraba nada al respecto. Sigue así, muchas gracias! 🙂
Sobresaliente , Impecables los conceptos !!
buen articulo, era justo lo que estaba buscando, gracias!!
Hola, estoy intentando hacer una app hibrida, con html5, jquery, jquery mobile, javascript, css3, php, y mysql.
En una parte de la app saco una foto con la camara o la tomo de la galeria, subo esa foto a un servidor ftp en internet y cuando quiero recuperar esa url me da undefined, debido a que envia a subir el archivo de forma asincronica y cuando quiero recuperar el valor de la url, para luego ingresarlo en un campo de la base de datos no puedo?.
Como puedo hacer para que se ejecute la subida de la foto, recibir la respuesta de la url para luego poder almacenarla.
Almacenarla no es un problema. el problema que tengo es la respuesta del servidor que pareciera ser lenta.
envio parte del codigo….
en el if valido que en el formulario se ingresen datos validos , luego subo la foto al http://ftp.. (lo agrego al final como lo hago)
y por ultimo guardo en la base de datos dentro del ajax, a taves del php… (no se si se entiende)
Si me pueden dar una mano lo agradeceria…
Es relativamente sencillo resolver tu problema. El método upload() del objeto FileTransfert tiene un callback que será llamado cuando la subida del fichero haya concluido. Tu has puesto ahí la funcíón subirModificarUsuarioOk y es en esa función donde debes llamar a modificarUsuario(), en vez de en el primer if.
Excelente articulo, muchas gracias.