Evitando Callback Hell en Node.js

Admito que fui una de esas personas que decidió aprender Node.js simplemente por el revuelo en torno a él y por lo mucho que todo el mundo hablaba de él. me imagino...

Introducción

Admito que fui una de esas personas que decidió aprender Node.js simplemente por el revuelo en torno a él y por lo mucho que todo el mundo hablaba de él. Pensé que debe haber algo especial en él si tiene tanto apoyo tan temprano en su vida. En su mayoría provenía de un entorno C, Java y Python, por lo que el estilo asíncrono de JavaScript era muy diferente a todo lo que había encontrado antes.

Como muchos de ustedes probablemente saben, todo el JavaScript que realmente se encuentra debajo es un ciclo de eventos de un solo subproceso que procesa los eventos en cola. Si tuviera que ejecutar una tarea de ejecución prolongada dentro de un solo subproceso, el proceso se bloquearía, lo que provocaría que otros eventos tuvieran que esperar para ser procesados ​​(es decir, la interfaz de usuario se cuelga, los datos no se guardan, etc.). Esto es exactamente lo que desea evitar en un sistema basado en eventos. Aquí es un gran video que explica mucho más sobre el bucle de eventos de JavaScript.

Para resolver este problema de bloqueo, JavaScript se basa en gran medida en las devoluciones de llamada, que son funciones que se ejecutan después de que finaliza un proceso de ejecución prolongada (IO, temporizador, etc.), lo que permite que la ejecución del código avance más allá de la tarea de ejecución prolongada.

1
2
3
downloadFile('example.com/weather.json', function(err, data) {
    console.log('Got weather data:', data);
});

El problema: Callback hell

Si bien el concepto de devoluciones de llamada es excelente en teoría, puede conducir a un código realmente confuso y difícil de leer. Imagínese si necesita hacer una devolución de llamada después de la devolución de llamada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){ 
            getMoreData(c, function(d){ 
                getMoreData(d, function(e){ 
                    ...
                });
            });
        });
    });
});

Como puede ver, esto realmente puede salirse de control. Agregue algunas declaraciones if, bucles for, llamadas a funciones o comentarios y tendrá un código muy difícil de leer. Los principiantes son especialmente víctimas de esto, sin entender cómo evitar esta "pirámide de la perdición".

Alternativas

Diseño a su alrededor

Tantos programadores quedan atrapados en el infierno de las devoluciones de llamada solo por este (mal diseño). Realmente no piensan en la estructura de su código con anticipación y no se dan cuenta de lo mal que se ha vuelto su código hasta que es demasiado tarde. Al igual que con cualquier código que esté escribiendo, debe detenerse y pensar qué se puede hacer para que sea más simple y más legible antes o mientras lo escribe. Aquí hay algunos consejos que puede usar para evitar el infierno de devolución de llamada (o al menos administrarlo).

Usar módulos

En casi todos los lenguajes de programación, una de las mejores formas de reducir la complejidad es modularizar. La programación JavaScript no es diferente. Siempre que esté escribiendo código, tómese un tiempo para dar un paso atrás y averiguar si ha habido un patrón común que encuentre con frecuencia.

¿Estás escribiendo el mismo código varias veces en diferentes lugares? ¿Las diferentes partes de su código siguen un tema común? Si es así, tiene la oportunidad de limpiar las cosas y abstraer y reutilizar el código.

Hay miles de módulos que puede consultar como referencia, pero aquí hay algunos para considerar. Manejan tareas comunes, pero muy específicas, que de otro modo desordenarían su código y reducirían la legibilidad: Pluralizar, [CSV](https://www.npmjs.com/ paquete/csv), qs, clon.

Asigne nombres a sus funciones

Al leer código (especialmente código desordenado y desorganizado), es fácil perder la pista del flujo lógico, o incluso la sintaxis, cuando los espacios pequeños están congestionados con tantas devoluciones de llamadas anidadas. Una forma de ayudar a combatir esto es nombrar sus funciones, por lo que todo lo que tendrá que hacer es mirar el nombre y tendrá una mejor idea de lo que hace. También le da a sus ojos un punto de referencia de sintaxis.

Considere el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, function(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

Mirar esto puede llevarte unos segundos para darte cuenta de lo que hace cada devolución de llamada y dónde comienza. Agregar un poco de información adicional (nombres) a las funciones puede marcar una gran diferencia en la legibilidad, especialmente cuando tiene varios niveles de devolución de llamadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function appendText(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, function notifyUser(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

Ahora, solo un vistazo rápido le dirá que la primera función agrega un texto mientras que la segunda función notifica al usuario sobre el cambio.

Declare sus funciones de antemano

Una de las mejores maneras de reducir el desorden de códigos es manteniendo una mejor separación de códigos. Si declara una función de devolución de llamada de antemano y la llama más tarde, evitará las estructuras profundamente anidadas que hacen que sea tan difícil trabajar con la devolución de llamada.

Así que podrías pasar de esto...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var fs = require('fs');

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', function(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, function(err) {
        if(err) return console.log(err);
        console.log('Appended text!');
    });
});

...a esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var fs = require('fs');

function notifyUser(err) {
    if(err) return console.log(err);
    console.log('Appended text!');
};

function appendText(err, txt) {
    if (err) return console.log(err);

    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt, notifyUser);
}

var myFile = '/tmp/test';
fs.readFile(myFile, 'utf8', appendText);

Si bien esta puede ser una excelente manera de ayudar a aliviar el problema, no lo resuelve por completo. Al leer el código escrito de esta manera, si no recuerda exactamente qué hace cada función, tendrá que volver atrás y mirar cada una para volver sobre el flujo lógico, lo que puede llevar tiempo.

Asíncrono.js

Afortunadamente, existen bibliotecas como Async.js para tratar de frenar el problema. Async agrega una capa delgada de funciones sobre su código, pero puede reducir en gran medida la complejidad al evitar el anidamiento de devolución de llamada.

Existen muchos métodos auxiliares en Async que se pueden usar en diferentes situaciones, como serie, [paralela](https://github.com/caolan/ async#parallel), cascada, etc. Cada función tiene un caso de uso específico, así que tómate un tiempo para aprender cuál te ayudará en qué situaciones.

Tan bueno como es Async, como todo, no es perfecto. Es muy fácil dejarse llevar por la combinación de series, paralelos, para siempre, etc., momento en el que vuelve al punto de partida con el código desordenado. Tenga cuidado de no optimizar prematuramente. El hecho de que algunas tareas asíncronas se puedan ejecutar en paralelo no siempre significa que deban hacerlo. En realidad, dado que Node es solo de subproceso único, la ejecución de tareas en paralelo al usar Async tiene poca o ninguna ganancia de rendimiento.

El código de arriba se puede simplificar usando la cascada de Async:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var fs = require('fs');
var async = require('async');

var myFile = '/tmp/test';

async.waterfall([
    function(callback) {
        fs.readFile(myFile, 'utf8', callback);
    },
    function(txt, callback) {
        txt = txt + '\nAppended something!';
        fs.writeFile(myFile, txt, callback);
    }
], function (err, result) {
    if(err) return console.log(err);
    console.log('Appended text!');
});

Promesas

Aunque Promises puede tomar un poco de comprensión, en mi opinión, son uno de los conceptos más importantes que puedes aprender en JavaScript. Durante el desarrollo de una de mis aplicaciones SaaS, terminé reescribiendo todo el código base usando Promises. No solo redujo drásticamente el número de líneas de código, sino que hizo que el flujo lógico del código fuera mucho más fácil de seguir.

Aquí hay un ejemplo usando la muy rápida y muy popular biblioteca Promise, Azulejo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var Promise = require('bluebird');
var fs = require('fs');
Promise.promisifyAll(fs);

var myFile = '/tmp/test';
fs.readFileAsync(myFile, 'utf8').then(function(txt) {
    txt = txt + '\nAppended something!';
    fs.writeFile(myFile, txt);
}).then(function() {
    console.log('Appended text!');
}).catch(function(err) {
    console.log(err);
});

Observe cómo esta solución no solo es más corta que las soluciones anteriores, sino que también es más fácil de leer (aunque, es cierto, puede llevar un tiempo acostumbrarse al código de estilo Promise). Tómese el tiempo para aprender y comprender Promises, valdrá la pena. Sin embargo, Promises definitivamente no es la solución a todos nuestros problemas en la programación asíncrona, así que no asuma que al usarlos tendrá una aplicación rápida, limpia y libre de errores. La clave es saber cuándo te serán útiles.

Algunas bibliotecas de Promise que debería consultar son q, Azulejo o promesas incorporadas si está utilizando ES6.

Asíncrono/Espera

Nota: Esta es una característica de ES7, que actualmente no es compatible con Node o io.js. Sin embargo, puede usarlo ahora mismo con un transpilador como Babel.

Otra opción para limpiar su código, y mi futura favorita (cuando tenga un soporte más amplio), es usar funciones async. Esto le permitirá escribir código que se parece mucho más al código síncrono, pero aún así es asíncrono.

Un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
async function getUser(id) {
    if (id) {
        return await db.user.byId(id);
    } else {
        throw 'Invalid ID!';
    }
}

try {
    let user = await getUser(123);
} catch(err) {
    console.error(err);
}

La llamada db.user.byId(id) devuelve una Promesa, que normalmente tendríamos que usar con .then(), pero con await podemos devolver el valor resuelto directamente.

Tenga en cuenta que la función que contiene la llamada await tiene el prefijo async, lo que nos dice que contiene código asíncrono y también debe llamarse con await.

Otra gran ventaja de este método es que ahora podemos usar try/catch, for y while con nuestras funciones asincrónicas, lo cual es mucho más intuitivo que encadenar promesas.

Además de usar transpilers como Babel y rastreador, también puede obtener una funcionalidad como esta en Node con el [asincaesperar](https://www.npmjs.com /paquete/asyncawait) paquete.

Conclusión

Evite problemas tan comunes como el infierno de la devolución de llamada no es fácil, así que no espere terminar con sus frustraciones de inmediato. Todos quedamos atrapados en él. Solo intente reducir la velocidad y tómese un tiempo para pensar en la estructura de su código. Como todo, la práctica hace al maestro.

  • ¿Has corrido al infierno de devolución de llamada? Si es así, ¿cómo evitarlo? ¡Dinos en los comentarios!* !*