Seleccionar página

asincronoLa 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 ?');

ejecutar…

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 ?');

ejecutar…

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');
        }
    });
}

ejecutar…

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');
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });
});

ejecutar…

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);
    }
});

ejecutar…

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
);

ejecutar…

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 ?');

ejecutar…

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');
});

ejecutar…

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);
});

ejecutar…

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');
});

ejecutar…

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();

ejecutar…

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();

ejecutar…

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);
    }
});

ejecutar…

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();

ejecutar…

La verdad es que se parece mucho al uso de co(), aunque con algunas diferencias:

  • En vez de co(function* ... utilizaremos async function...
  • En vez de yield utilizaremos await
  • 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 usar co.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);
})();

ejecutar…

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)

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

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

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

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.