Uso de Step
Para una ejecución en serie, hay que pasar
this
como el argumento de callback de la función asíncrona o bien devolver un valor si se trata de un código sincrono. El siguiente ejemplo de un código que se lee a sí mismo y se saca por consola en mayúsculas ilustra lo que quiero decir:Step( function meLeo(){ //Lee el codigo fuente de este fichero fs.readFile(__filename, this); }, function pasarAMayusculas(err, text){ //Ejemplo de código síncrono if(err) throw err; return text.toUpperCase(); }, function mostrar(err, texto){ if(err) throw err; console.log(texto); } )
Siguiendo el estándar de node.js (la convención que mencioné en el artículo anterior), los callbacks tienen siempre como primer argumento el error. Además, si no se definen callbacks en línea, sino que se le pasan en serie a Step, tenemos garantizada la captura de todas las excepciones, con la seguridad que ello conlleva.
El ejemplo anterior realiza tareas en serie. Realizar tareas en paralelo es igual de sencillo:
Step( //Cargar dos ficheros en paralelo function cargarFicheros(){ fs.readFile(__filename, this.parallel()); fs.readFile("/etc/passwd", this.parallel()); }, function mostrarContenido(err, codigo, usuarios){ if(err) throw err; console.log(codigo); console.log(usuarios); } )
Al usar
this.parallel()
, Step llevará, de manera transparente, la cuenta del número y orden de los callbacks. Al terminar la ejecución en paralelo, pasará al siguiente callback como parámetros los resultados de las tareas paralelizadas, ajustadas al orden en que fueron invocadas estas tareas.Estructura de Step
Vamos a revisar como está estructurada esta librería para buscar elementos reseñables.
Step viene con un README detallado, un fichero package.json, un único fichero para la librería principal, y los tests separados en diferentes ficheros que abordan cada uno una característica principal de la libreria.
El soporte del módulo CommonJS es condicional, lo que permite usar esta librería fuera de Node (por ejemplo, en un navegador):
// Hook into commonJS module systems if (typeof module !== 'undefined' && "exports" in module) { module.exports = Step; }
La estructura base de la principal función de Step es muy fácil de seguir:
function Step() { var steps = Array.prototype.slice.call(arguments), pending, counter, results, lock; // Define the main callback that's given as `this` to the steps. function next() { // ... } // Add a special callback generator `this.parallel()` that groups stuff. next.parallel = function () { // ... }; // Generates a callback generator for grouped results next.group = function () { // ... }; // Start the engine an pass nothing to the first step. next(); }
La función
next
se invoca al final de Step y es lo que arranca la ejecución. Esta función también tiene los métodos parallel
y group
a los que se puede acceder desde el this
en los callbacks.
Gestión de la ejecución
El núcleo de la librería es la función
next
. Veamos cada una de sus partes principales.Los contadores y los valores de retorno se usan para determinar que hay que ejecutar a continuación. El contador se usa en
parallel
y group
. Estos valores se establecen cuando se llama a next
:// Define the main callback that's given as `this` to the steps. function next() { counter = pending = 0;
El array de funciones pasados a Step se ejecutan en orden invocando shift en el array. Si no hay más pasos, si hay errores no gestionados se lanzan y si no la ejecución se completa:
// Check if there are no steps left if (steps.length === 0) { // Throw uncaught errors if (arguments[0]) { throw arguments[0]; } return; } // Get the next step to execute var fn = steps.shift(); results = [];
Cada "step" se invoca utilizando
apply
de manera que el this
en la función suministrada sea "next":// Run the step in a try..catch block so exceptions don't get out of hand. try { lock = true; var result = fn.apply(next, arguments); } catch (e) { // Pass any exceptions on through the next callback next(e); }
Los errores se capturan y se pasan al siguiente paso. La variable
lock
se usa por la funcionalidad de paralelización de tareas y agrupación de resultados. El valor devuelto de la función del paso se guarda. A continuación, el valor devuelto se utiliza para determinar si se ha utilizado una función síncrona y en caso afirmativo se invoca next
de nuevo con el resultado:
if (result !== undefined) { if (counter > 0 && pending == 0) { // If parallel() was called, and all parallel branches executed // syncronously, go on to the next step immediately. next.apply(null, results); } else if (result !== undefined) { // If a syncronous return is used, pass it to the callback next(undefined, result); } lock = false;
Ejecución paralela
El método
parallel
devuelve una función que recubre los callback para mantener los contadores y ejecutar el siguiente paso. El valor de la variable index
queda fijada en cada llamada a parallel()
y se utiliza para almacenar los valores devueltos por la funciones que se ejecutan en paralelo en las posición correcta:
// Add a special callback generator `this.parallel()` that groups stuff. next.parallel = function () { var index = 1 + counter++; pending++; return function () { pending--; // Compress the error from any result to the first argument if (arguments[0]) { results[0] = arguments[0]; } // Send the other results as arguments results[index] = arguments[1]; if (!lock && pending === 0) { // When all parallel branches done, call the callback next.apply(null, results); } };
Step.fn
La función Step.fn (sin documentar) crea factorías de función a partir de las llamadas a step. Se usa de esta forma:
var myfn = Step.fn( function (name) { fs.readFile(name, 'utf8', this); }, function capitalize(err, text) { if (err) throw err; return text.toUpperCase(); } ); var selfText = fs.readFileSync(__filename, 'utf8'); expect('result'); myfn(__filename, function (err, result) { fulfill('result'); if (err) throw err; assert.equal(selfText.toUpperCase(), result, "It should work"); });
Si el último argumento utilizado al llamar al conjunto de pasos agrupados en
myfn
es una función, esta se ejecutará como el último paso.
Últimas notas
Step lleva circulando ya un tiempo y Tim a estado trabajando activamente en ella. Es una librería pequeña que resuelve un problema habitual de control de flujos asíncronos. Según el registro de NPM, unas 45 librerías dependen de Step, y esto es sin duda un buen indicador de la popularidad de esta librería.
Hola
ResponderEliminarParece ser, por lo que se ve en su Git, que Step ha sido abandonada, ¿verdad?
¿Qué reemplazo recomendarías? Parece que Async ha tomado bastante fuerza...
¿O optarías por usar alguna librería que implementara promesas, como Q?
Muchas gracias
La verdad es que ahora apenas usamos este patrón de programación y cuando lo usamos utilizamos async. Efectivamente, ahora estamos usando mucho las promesas, y la librería Q es la que más usamos ($q con AngularJS). Para nosotros, unas funciones fundamentales de Q cuando usas NodeJS son fnapply y fncall, para usar la api de NodeJS como si fueran promesas.
ResponderEliminar