Solución de problemas de devolución de llamada con Async
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:
-
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.
-
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.
-
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:



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.



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.



