1. Code
  2. JavaScript
  3. Node

Solución de problemas de devolución de llamada con Async

Ahora, digamos que desea mostrar el contenido de dos archivos en un orden particular. Terminarás con algo como esto:
Scroll to top

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Cuando comenzamos a programar, nos enteramos de que un bloque de código se ejecuta desde arriba hacia abajo. Esta es una programación sincrónica: cada operación se completa antes de que comience la siguiente. Esto es genial cuando haces muchas cosas que la computadora no tarda prácticamente en completar, como agregar números, manipular una cadena o asignar variables.

¿Qué sucede cuando quieres hacer algo que lleva un tiempo relativamente largo, como acceder a un archivo en el disco, enviar una solicitud de red o esperar a que pase el temporizador? En la programación sincrónica, su secuencia de comandos no puede hacer nada más mientras espera.

Esto podría estar bien para algo simple o en una situación en la que tendrá varias instancias de secuencia de comandos en ejecución, pero para muchas aplicaciones de servidor, es una pesadilla.

Ingrese a la programación asíncrona. En una secuencia de comandos asincrónica, su código continúa ejecutándose mientras espera que ocurra algo, pero puede retroceder cuando algo ha sucedido.

Tome, por ejemplo, una solicitud de red. Si realiza una solicitud de red a un servidor lento que tarda tres segundos completos en responder, su secuencia de comandos puede estar haciendo otras cosas activamente mientras este servidor lento responde. En este caso, tres segundos podrían no parecerle mucho a un ser humano, pero un servidor podría responder a miles de solicitudes más mientras espera. Entonces, ¿cómo manejas la asincronía en Node.js?

La forma más básica es a través de una devolución de llamada. Una devolución de llamada es solo una función que se llama cuando se completa una operación asincrónica. Por convención, las funciones de devolución de llamada de Node.js tienen al menos un argumento, err. Las devoluciones de llamada pueden tener más argumentos (que generalmente representan los datos devueltos a la devolución de llamada), pero el primero será err. Como habrás adivinado, errar contiene un objeto err (si se ha desencadenado un error, más sobre eso más adelante).

Echemos un vistazo a un ejemplo muy simple. Utilizaremos el módulo de sistema de archivos integrado (fs) de Node.js. En este script, leeremos el contenido de un archivo de texto. La última línea del archivo es console.log que plantea una pregunta: si ejecuta este script, ¿cree que verá el registro antes de que veamos el contenido del archivo de texto?

1
var
2
  fs  = require('fs');
3
4
fs.readFile(
5
  'a-text-file.txt',      //the filename of a text file that says "Hello!"

6
  'utf8',                 //the encoding of the file, in this case, utf-8

7
  function(err,text) {    //the callback

8
    console.log('Error:',err);    //Errors, if any

9
    console.log('Text:',text);    //the contents of the file

10
  }
11
);
12
//Will this be before or after the Error / Text?

13
console.log('Does this get logged before or after the contents of the text file?'); 

Como esto es asincrónico, veremos el último console.log antes del contenido del archivo de texto. Si tiene un archivo llamado a-text-file.txt en el mismo directorio en el que está ejecutando su script de nodo, verá que err es null, y el valor del text se rellena con el contenido del archivo de texto.

Si no tiene un archivo llamado a-text-file.txt, err devolverá un objeto Error, y el valor del text estará undefined. Esto lleva a un aspecto importante de las devoluciones de llamada: siempre debe manejar sus errores. Para manejar los errores, debe verificar un valor en la variable err; si hay un valor presente, se produjo un error. Por convención, los argumentos err por lo general no devuelven false, por lo que puede verificar solo la veracidad.

1
var
2
  fs  = require('fs');
3
4
fs.readFile(
5
  'a-text-file.txt',      //the filename of a text file that says "Hello!"

6
  'utf8',                 //the encoding of the file, in this case, utf-8

7
  function(err,text) {    //the callback

8
    if (err) {
9
      console.error(err);           //display an error to the console

10
    } else {
11
      console.log('Text:',text);    //no error, so display the contents of the file

12
    }
13
  }
14
);

Ahora, digamos que desea mostrar el contenido de dos archivos en un orden particular. Terminarás con algo como esto:

1
var
2
  fs  = require('fs');
3
4
fs.readFile(
5
  'a-text-file.txt',      //the filename of a text file that says "Hello!"

6
  'utf8',                 //the encoding of the file, in this case, utf-8

7
  function(err,text) {    //the callback

8
    if (err) {
9
      console.error(err);           //display an error to the console

10
    } else {
11
      console.log('First text file:',text);    //no error, so display the contents of the file

12
      fs.readFile(
13
        'another-text-file.txt',  //the filename of a text file that says "Hello!"

14
        'utf8',                   //the encoding of the file, in this case, utf-8

15
        function(err,text) {      //the callback

16
          if (err) {
17
            console.error(err);                       //display an error to the console

18
          } else {
19
            console.log('Second text file:',text);    //no error, so display the contents of the file

20
          }
21
        }
22
      );
23
    }
24
  }
25
);

El código se ve bastante desagradable y tiene una serie de problemas:

  1. Está cargando los archivos secuencialmente; Sería más eficiente si pudiera cargar ambos al mismo tiempo y devolver los valores cuando ambos se hayan cargado por completo.    

  2. Sintácticamente es correcto pero difícil de leer. Observe la cantidad de funciones anidadas y las pestañas en aumento. Podría hacer algunos trucos para que se vea un poco mejor, pero puede estar sacrificando la legibilidad de otras maneras.

  3. No es un propósito muy general. Esto funciona bien para dos archivos, pero ¿qué pasaría si tuviera nueve archivos algunas veces y otras veces 22 o solo uno? La forma en que está escrito actualmente es muy rígido.

No se preocupe, podemos resolver todos estos problemas (y más) con async.js.

Devolución de llamada con Async.js

Primero, comencemos instalando el módulo async.js.

1
npm install async —-save

Async.js se puede utilizar para unir conjuntos de funciones en series o en paralelo. Vamos a reescribir nuestro ejemplo:

1
var
2
  async = require('async'),     //async.js module

3
  fs    = require('fs');
4
5
async.series(                     //execute the functions in the first argument one after another

6
  [                               //The first argument is an array of functions

7
    function(cb) {                //`cb` is shorthand for "callback"

8
      fs.readFile(
9
        'a-text-file.txt', 
10
        'utf8',
11
        cb
12
      );
13
    },
14
    function(cb) {
15
      fs.readFile(
16
        'another-text-file.txt', 
17
        'utf8',
18
        cb
19
      );
20
    }
21
  ],
22
  function(err,values) {          //The "done" callback that is ran after the functions in the array have completed

23
    if (err) {                    //If any errors occurred when functions in the array executed, they will be sent as the err.

24
      console.error(err);
25
    } else {                      //If err is falsy then everything is good

26
      console.log('First text file:',values[0]);
27
      console.log('Second text file:',values[1]);
28
    }
29
  }
30
);

Esto funciona casi como en el ejemplo anterior, cargando secuencialmente cada archivo, y solo difiere en que lee cada archivo y no muestra el resultado hasta que se completa. El código es más conciso y más limpio que el ejemplo anterior (y lo haremos aún mejor más adelante). async.series toma una serie de funciones y las ejecuta una tras otra.

Cada función solo debe tener un único argumento, la devolución de llamada (o cb en nuestro código). cb debe ejecutarse con el mismo tipo de argumentos que cualquier otra devolución de llamada, por lo que podemos ponerlo directamente en nuestros argumentos fs.readFile.

Finalmente, los resultados se envían a la devolución de llamada final, el segundo argumento a async.series. Los resultados se almacenan en una matriz con los valores correlacionados con el orden de las funciones en el primer argumento de async.series.

Con async.js, el manejo de errores se simplifica porque si encuentra un error, devuelve el error al argumento de la devolución de llamada final y no ejecutará ninguna otra función asincrónica.

Todo junto ahora

Una función relacionada es async.parallel; tiene los mismos argumentos que async.series por lo que puede cambiar entre los dos sin alterar el resto de su sintaxis. Este es un buen punto para cubrir paralelo versus concurrente.

JavaScript es básicamente un lenguaje de subproceso único, lo que significa que solo puede hacer una cosa a la vez. Es capaz de realizar algunas tareas en un hilo separado (la mayoría de las funciones de E / S, por ejemplo), y aquí es donde la programación asíncrona entra en juego con JS. No confunda el paralelismo con la concurrencia.

Cuando ejecutas dos cosas con async.parallel, no estás abriendo otro hilo para analizar JavaScript o hacer dos cosas a la vez: estás realmente controlando cuando pasa entre funciones en el primer argumento de async.parallel. Entonces no ganas nada simplemente poniendo código sincrónico en async.parallel.

Esto se explica mejor visualmente:

Explaining asynchronous programmingExplaining asynchronous programmingExplaining asynchronous programming

Aquí está nuestro ejemplo anterior escrito para ser paralelo: la única diferencia es que usamos async.parallel en lugar de async.series.

1
var
2
  async = require('async'),       //async.js module

3
  fs    = require('fs');
4
5
async.parallel(                   //execute the functions in the first argument, but don't wait for the first function to finish to start the second

6
  [                               //The first argument is an array of functions

7
    function(cb) {                //`cb` is shorthand for "callback"

8
      fs.readFile(
9
        'a-text-file.txt', 
10
        'utf8',
11
        cb
12
      );
13
    },
14
    function(cb) {
15
      fs.readFile(
16
        'another-text-file.txt', 
17
        'utf8',
18
        cb
19
      );
20
    }
21
  ],
22
  function(err,values) {          //The "done" callback that is ran after the functions in the array have completed

23
    if (err) {                    //If any errors occurred when functions in the array executed, they will be sent as the err.

24
      console.error(err);
25
    } else {                      //If err is falsy then everything is good

26
      console.log('First text file:',values[0]);
27
      console.log('Second text file:',values[1]);
28
    }
29
  }
30
);

Una y otra vez

Nuestros ejemplos anteriores han ejecutado un número fijo de operaciones, pero ¿qué sucede si necesita una cantidad variable de operaciones asincrónicas? Esto se complica rápidamente si solo recurre a devoluciones de llamada y construcciones regulares de lenguaje, confiando en contadores torpes o verificaciones de condición que oscurecen el significado real de su código. Echemos un vistazo al equivalente aproximado de un bucle for con async.js.

En este ejemplo, escribiremos diez archivos en el directorio actual con nombres de archivo secuenciales y algunos contenidos breves. Puede variar el número cambiando el valor del primer argumento de async.times. En este ejemplo, la devolución de llamada para fs.writeFile solo crea un argumento err, pero la función async.times también puede admitir un valor de retorno. Al igual que async.series, se pasa a la devolución de llamada realizada en el segundo argumento como una matriz.

1
var
2
  async = require('async'),
3
  fs    = require('fs');
4
5
async.times(
6
  10,                                   // number of times to run the function 

7
  function(runCount,callback) {
8
    fs.writeFile(
9
      'file-'+runCount+'.txt',          //the new file name

10
      'This is file number '+runCount,  //the contents of the new file

11
      callback
12
    );
13
  },
14
  function(err) {
15
    if (err) {
16
      console.error(err);
17
    } else {
18
      console.log('Wrote files.');
19
    }
20
  }
21
);

Es un buen momento para decir que la mayoría de las funciones de async.js, por defecto, se ejecutan en paralelo en lugar de series. Entonces, en el ejemplo anterior, comenzará a crear los archivos e informará cuando todos estén completamente creados y escritos.

Esas funciones que se ejecutan en paralelo de forma predeterminada tienen una función de serie corolario indicada por la función que termina con, lo adivinó, 'Serie'. Por lo tanto, si quisiera ejecutar este ejemplo en series en lugar de paralelas, cambiaría async.times a async.timesSeries.

Para nuestro próximo ejemplo de bucle, vamos a echar un vistazo a la función async.until. async.until ejecuta una función asíncrona (en serie) hasta que se cumple una condición particular. Esta función toma tres funciones como argumentos.

La primera función es la prueba en la que se devuelve verdadero (si desea detener el ciclo) o falso (si desea continuar el ciclo). El segundo argumento es la función asíncrona, y el final es la devolución de llamada realizada. Echale un vistazo a éste ejemplo:

1
var
2
  async     = require('async'),
3
  fs        = require('fs'),
4
  startTime = new Date().getTime(),   //the unix timestamp in milliseconds

5
  runCount  = 0;
6
7
async.until(
8
  function () {
9
    //return true if 4 milliseconds have elapsed, otherwise false (and continue running the script)

10
    return new Date().getTime() > (startTime + 5);
11
  },
12
  function(callback) {
13
    runCount += 1;
14
    fs.writeFile(
15
      'timed-file-'+runCount+'.txt',    //the new file name

16
      'This is file number '+runCount,  //the contents of the new file

17
      callback
18
    );
19
  },
20
  function(err) {
21
    if (err) {
22
      console.error(err);
23
    } else {
24
      console.log('Wrote files.');
25
    }
26
  }
27
);

Este script creará nuevos archivos de texto por cinco milisegundos. Al comienzo del script, obtenemos la hora de inicio en el milisegundo de Unix, y luego en la función de prueba obtenemos la hora actual y la prueba para ver si es cinco milisegundos mayor que la hora de inicio más cinco. Si ejecuta este script varias veces, puede obtener resultados diferentes.

En mi máquina, estaba creando entre 6 y 20 archivos en cinco milisegundos. Curiosamente, si intentas agregar console.log a la función de prueba o a la función asíncrona, obtendrás resultados muy diferentes, ya que lleva tiempo escribir en tu consola. ¡Simplemente va a mostrar que en el software, todo tiene un costo de rendimiento!

El para cada bucle es una estructura práctica: le permite hacer algo para cada elemento de una matriz. En async.js, esta sería la función async.each. Esta función toma tres argumentos: la colección o matriz, la función asíncrona para realizar para cada elemento y la devolución de llamada realizada.

En el ejemplo a continuación, tomamos una serie de cadenas (en este caso, tipos de razas de perros de raza) y creamos un archivo para cada cadena. Cuando se hayan creado todos los archivos, se ejecutará la devolución de llamada realizada. Como era de esperar, los errores se manejan a través del objeto err en la devolución de llamada realizada. async.each se ejecuta en paralelo, pero si desea ejecutarlo en serie, puede seguir el patrón mencionado anteriormente y utilizar async.eachSeries en lugar de async.each.

1
var
2
  async     = require('async'),
3
  fs        = require('fs');
4
5
async.each(
6
  //an array of sighthound dog breeds

7
  ['greyhound','saluki','borzoi','galga','podenco','whippet','lurcher','italian-greyhound'],
8
  function(dogBreed, callback) {
9
    fs.writeFile(
10
      dogBreed+'.txt',                         //the new file name

11
      'file for dogs of the breed '+dogBreed,  //the contents of the new file

12
      callback
13
    );
14
  },
15
  function(err) {
16
    if (err) {
17
      console.error(err);
18
    } else {
19
      console.log('Done writing files about dogs.');
20
    }
21
  }
22
);

Un primo de async.each es la función async.map; la diferencia es que puede pasar los valores a su devolución de llamada hecha. Con la función async.map, transfiere una matriz o colección como primer argumento, y luego se ejecutará una función asincrónica en cada elemento de la matriz o colección. El último argumento es la devolución de llamada realizada.

El siguiente ejemplo toma la matriz de razas de perros y usa cada elemento para crear un nombre de archivo. El nombre del archivo se pasa a fs.readFile, donde se lee y la función de devolución de llamada devuelve los valores. Usted termina con una matriz de contenido de archivos en los argumentos de devolución de llamada realizados.

1
var
2
  async     = require('async'),
3
  fs        = require('fs');
4
5
async.map(
6
  ['greyhound','saluki','borzoi','galga','podenco','whippet','lurcher','italian-greyhound'],
7
  function(dogBreed, callback) {
8
    fs.readFile(
9
      dogBreed+'.txt',    //the new file name

10
      'utf8',
11
      callback
12
    );
13
  },
14
  function(err, dogBreedFileContents) {
15
    if (err) {
16
      console.error(err);
17
    } else {
18
      console.log('dog breeds');
19
      console.log(dogBreedFileContents);
20
    }
21
  }
22
);

async.filter también es muy similar en sintaxis a async.each y async.map, pero con el filtro se envía un valor booleano a la devolución de llamada del artículo en lugar del valor del archivo. En la devolución de llamada realizada, obtienes una nueva matriz, con solo los elementos por los que pasaste un valor true o truthy en la devolución de llamada del artículo.

1
var
2
  async     = require('async'),
3
  fs        = require('fs');
4
5
async.filter(
6
  ['greyhound','saluki','borzoi','galga','podenco','whippet','lurcher','italian-greyhound'],
7
  function(dogBreed, callback) {
8
    fs.readFile(
9
      dogBreed+'.txt',    //the new file name

10
      'utf8',
11
      function(err,fileContents) {
12
        if (err) { callback(err); } else {
13
          callback(
14
            err,                                //this will be falsy since we checked it above

15
            fileContents.match(/greyhound/gi)   //use RegExp to check for the string 'greyhound' in the contents of the file

16
          );
17
        }
18
      }
19
    );
20
  },
21
  function(err, dogBreedFileContents) {
22
    if (err) {
23
      console.error(err);
24
    } else {
25
      console.log('greyhound breeds:');
26
      console.log(dogBreedFileContents);
27
    }
28
  }
29
);

En este ejemplo, estamos haciendo algunas cosas más que en los ejemplos anteriores. Observe cómo estamos agregando una llamada de función adicional y manejando nuestro propio error. El patrón if err y callback(err) es muy útil si necesita manipular los resultados de una función asincrónica, pero aún desea que async.js maneje los errores.

Además, notará que estamos utilizando la variable err como primer argumento para la función de devolución de llamada. A primera vista, esto no se ve del todo bien. Pero como ya hemos comprobado la veracidad del error, sabemos que es falsy y es seguro pasar a la devolución de llamada.

Sobre el borde de un acantilado

Hasta ahora, hemos explorado una serie de componentes útiles que tienen corolarios en la programación sincrónica. Vamos a sumergirnos en async.waterfall, que no tiene mucho equivalente en el mundo síncrono.

El concepto con una cascada es que los resultados de una función asincrónica fluyen a los argumentos de otra función asíncrona en serie. Es un concepto muy poderoso, especialmente cuando se trata de unir múltiples funciones asincrónicas que dependen unas de otras. Con async.waterfall, el primer argumento es una matriz de funciones, y el segundo argumento es la devolución de llamada realizada.

En su conjunto de funciones, la primera función siempre comenzará con un único argumento, la devolución de llamada. Cada función posterior debe coincidir con los argumentos que no son err de la función anterior sin la función err y con la adición de la nueva devolución de llamada.

waterfall examplewaterfall examplewaterfall example

En nuestro próximo ejemplo, comenzaremos a combinar algunos conceptos usando cascada como pegamento. En la matriz que es el primer argumento, tenemos tres funciones: la primera carga la lista de directorios desde el directorio actual, la segunda toma la lista de directorios y usa async.map para ejecutar fs.stat en cada archivo, y la tercera función toma la lista del directorio del resultado de la primera función y obtiene el contenido de cada archivo (fs.readFile).

async.waterfall ejecuta cada función secuencialmente, por lo que siempre ejecutará todas las funciones fs.stat antes de ejecutar cualquier fs.readFile. En este primer ejemplo, las funciones segunda y tercera no son dependientes entre sí, por lo que podrían incluirse en un async.parallel para reducir el tiempo total de ejecución, pero modificaremos esta estructura nuevamente para el siguiente ejemplo.

Nota: Ejecute este ejemplo en un pequeño directorio de archivos de texto; de lo contrario, obtendrá una gran cantidad de basura durante mucho tiempo en la ventana de su terminal.

1
var
2
  async     = require('async'),
3
  fs        = require('fs');
4
5
async.waterfall([
6
    function(callback) {
7
      fs.readdir('.',callback);               //read the current directory, pass it along to the next function.

8
    },
9
    function(fileNames,callback) {            //`fileNames` is the directory listing from the previous function

10
      async.map(                             
11
        fileNames,                             //The directory listing is just an array of filenames,

12
        fs.stat,                               //so we can use async.map to run fs.stat for each filename

13
        function(err,stats) {
14
          if (err) { callback(err); } else {
15
            callback(err,fileNames,stats);    //pass along the error, the directory listing and the stat collection to the next item in the waterfall

16
          }
17
        }
18
      );
19
    },
20
    function(fileNames,stats,callback) {      //the directory listing, `fileNames` is joined by the collection of fs.stat objects in  `stats`

21
      async.map(
22
        fileNames,
23
        function(aFileName,readCallback) {    //This time we're taking the filenames with map and passing them along to fs.readFile to get the contents

24
          fs.readFile(aFileName,'utf8',readCallback);
25
        },
26
        function(err,contents) {
27
          if (err) { callback(err); } else {  //Now our callback will have three arguments, the original directory listing (`fileNames`), the fs.stats collection and an array of with the contents of each file

28
            callback(err,fileNames,stats,contents);
29
          }
30
        }
31
      );
32
    }
33
  ],
34
  function(err, fileNames,stats,contents) {
35
    if (err) {
36
      console.error(err);
37
    } else {
38
      console.log(fileNames);
39
      console.log(stats);
40
      console.log(contents);
41
    }
42
  }
43
);

Digamos que queremos obtener los resultados de solo los archivos que tienen un tamaño superior a 500 bytes. Podríamos usar el código anterior, pero obtendría el tamaño y el contenido de cada archivo, ya sea que los necesite o no. ¿Cómo podría obtener la estadística de los archivos y solo los contenidos de los archivos que alcanzan los requisitos de tamaño?

Primero, podemos extraer todas las funciones anónimas en funciones nombradas. Es una preferencia personal, pero hace que el código sea un poco más limpio y fácil de entender (reutilizable para arrancar). Como se imaginará, tendrá que obtener los tamaños, evaluar esos tamaños y obtener solo el contenido de los archivos por encima del requisito de tamaño. Esto se puede lograr fácilmente con algo como Array.filter, pero esa es una función sincrónica, y async.waterfall espera funciones de estilo asíncrono. Async.js tiene una función de ayuda que puede envolver funciones sincrónicas en funciones asíncronas, el más bien jazzy llamado async.asyncify.

Necesitamos hacer tres cosas, todas las cuales envolveremos con async.asyncify. Primero, tomaremos el nombre de archivo y las matrices de estadísticas de la función arrayFsStat, y las fusionaremos usando el mapa map. Luego, filtraremos los elementos que tengan un tamaño de estadísticas inferior a 300. Finalmente, tomaremos el nombre de archivo combinado y el objeto stat, y utilizaremos el mapa map nuevamente para obtener el nombre del archivo.

Después de obtener los nombres de los archivos con un tamaño inferior a 300, utilizaremos async.map y fs.readFile para obtener los contenidos. Hay muchas maneras de romper este huevo, pero en nuestro caso se rompió para mostrar la máxima flexibilidad y la reutilización del código. Este uso async.waterfall ilustra cómo puede mezclar y combinar el código síncrono y asíncrono.

1
var
2
  async     = require('async'),
3
  fs        = require('fs');
4
5
//Our anonymous refactored into named functions 

6
function directoryListing(callback) {
7
  fs.readdir('.',callback);
8
}
9
10
function arrayFsStat(fileNames,callback) {
11
  async.map(
12
    fileNames,
13
    fs.stat,
14
    function(err,stats) {
15
      if (err) { callback(err); } else {
16
        callback(err,fileNames,stats);
17
      }
18
    }
19
  );
20
}
21
22
function arrayFsReadFile(fileNames,callback) {
23
  async.map(
24
    fileNames,
25
    function(aFileName,readCallback) { 
26
      fs.readFile(aFileName,'utf8',readCallback);
27
    },
28
    function(err,contents) {
29
      if (err) { callback(err); } else {
30
        callback(err,contents);
31
      }
32
    }
33
  );
34
}
35
36
//These functions are synchronous 

37
function mergeFilenameAndStat(fileNames,stats) {
38
  return stats.map(function(aStatObj,index) {
39
    aStatObj.fileName = fileNames[index];
40
    return aStatObj;
41
  });
42
}
43
44
function above300(combinedFilenamesAndStats) {
45
  return combinedFilenamesAndStats
46
    .filter(function(aStatObj) {
47
      return aStatObj.size >= 300;
48
    });
49
}
50
51
function justFilenames(combinedFilenamesAndStats) {
52
  return combinedFilenamesAndStats
53
    .map(function(aCombinedFileNameAndStatObj) {
54
      return aCombinedFileNameAndStatObj.fileName;
55
    });
56
}
57
58
    
59
async.waterfall([
60
    directoryListing,
61
    arrayFsStat,
62
    async.asyncify(mergeFilenameAndStat),   //asyncify wraps synchronous functions in a err-first callback

63
    async.asyncify(above300),
64
    async.asyncify(justFilenames),
65
    arrayFsReadFile
66
  ],
67
  function(err,contents) {
68
    if (err) {
69
      console.error(err);
70
    } else {
71
      console.log(contents);
72
    }
73
  }
74
);

Dando un paso más allá, refinemos nuestra función aún más. Digamos que queremos escribir una función que funcione exactamente como arriba, pero con la flexibilidad de mirar en cualquier camino. Un primo cercano a async.waterfall es async.seq. Mientras async.waterfall solo ejecuta una cascada de funciones, async.seq devuelve una función que realiza una cascada de otras funciones. Además de crear una función, puede pasar valores que irán a la primera función asincrónica.

La conversión a async.seq solo requiere algunas modificaciones. Primero, modificaremos directoryListing para aceptar un argumento: esta será la ruta. Segundo, agregaremos una variable para mantener nuestra nueva función (directoryAbove300). En tercer lugar, tomaremos el argumento array de async.waterfall y lo traduciremos en argumentos para async.seq. Nuestra devolución de llamada hecha para la cascada se usa ahora como devolución de llamada procesada cuando ejecutamos directoryAbove300.

1
var
2
  async     = require('async'),
3
  fs        = require('fs'),
4
  directoryAbove300;
5
6
function directoryListing(initialPath,callback) { //we can pass a variable into the first function used in async.seq - the resulting function can accept arguments and pass them this first function

7
  fs.readdir(initialPath,callback);
8
}
9
10
function arrayFsStat(fileNames,callback) {
11
  async.map(
12
    fileNames,
13
    fs.stat,
14
    function(err,stats) {
15
      if (err) { callback(err); } else {
16
        callback(err,fileNames,stats);
17
      }
18
    }
19
  );
20
}
21
22
function arrayFsReadFile(fileNames,callback) {
23
  async.map(
24
    fileNames,
25
    function(aFileName,readCallback) { 
26
      fs.readFile(aFileName,'utf8',readCallback);
27
    },
28
    function(err,contents) {
29
      if (err) { callback(err); } else {
30
        callback(err,contents);
31
      }
32
    }
33
  );
34
}
35
36
function mergeFilenameAndStat(fileNames,stats) {
37
  return stats.map(function(aStatObj,index) {
38
    aStatObj.fileName = fileNames[index];
39
    return aStatObj;
40
  });
41
}
42
43
function above300(combinedFilenamesAndStats) {
44
  return combinedFilenamesAndStats
45
    .filter(function(aStatObj) {
46
      return aStatObj.size >= 300;
47
    });
48
}
49
50
function justFilenames(combinedFilenamesAndStats) {
51
  return combinedFilenamesAndStats
52
    .map(function(aCombinedFileNameAndStatObj) {
53
      return aCombinedFileNameAndStatObj.fileName;
54
    })
55
}
56
57
//async.seq will produce a new function that you can use over and over

58
directoryAbove300 = async.seq(
59
  directoryListing,
60
  arrayFsStat,
61
  async.asyncify(mergeFilenameAndStat),
62
  async.asyncify(above300),
63
  async.asyncify(justFilenames),
64
  arrayFsReadFile
65
);
66
67
directoryAbove300(
68
  '.',
69
  function(err, fileNames,stats,contents) {
70
    if (err) {
71
      console.error(err);
72
    } else {
73
      console.log(fileNames);
74
    }
75
  }
76
);

Una nota sobre promesas y funciones asíncronas

Quizás se pregunte por qué no he mencionado las promesas. No tengo nada en contra de ellos; son bastante prácticos y tal vez sean una solución más elegante que las devoluciones de llamadas, pero son una forma diferente de ver la codificación asincrónica.

Los módulos incorporados de Node.js usan devoluciones de llamada err, y miles de otros módulos usan este patrón. De hecho, esa es la razón por la cual este tutorial usa fs en los ejemplos, algo tan fundamental como el acceso al sistema de archivos en Node.js usa devoluciones de llamada, por lo que disputar códigos de devolución de llamada sin promesas es una parte esencial de la programación de Node.js.

Es posible usar algo como Bluebird para envolver las primeras devoluciones de llamada erróneas en funciones basadas en Promise, pero eso solo lo lleva tan lejos: Async.js proporciona una serie de metáforas que hacen que el código asíncrono sea legible y manejable.

Embrace asincronía

JavaScript se ha convertido en uno de los idiomas de facto para trabajar en la web. No es sin sus curvas de aprendizaje, y hay muchos marcos y bibliotecas para mantenerte ocupado, también. Si está buscando recursos adicionales para estudiar o usar en su trabajo, consulte lo que tenemos disponible en el Envato marketplace.

Pero aprender asincrónicamente es algo completamente diferente, y con suerte, este tutorial te ha demostrado lo útil que puede ser.

La asincronía es clave para escribir JavaScript en el lado del servidor, pero si no está diseñado correctamente, tu código puede convertirse en una bestia inmanejable de devoluciones de llamadas. Al utilizar una biblioteca como async.js que proporciona varias metáforas, es posible que escribir código asíncrono sea una alegría.