Introdução à programação assíncrona do Node JS

Introducción a la programación asincrónica de Node JS

¡Domine la programación asincrónica de Node.js hoy! Mejore sus habilidades de desarrollo web con nuestra guía paso a paso fácil de entender.

Imagem em destaque

NodeJS, al subcontratar las tareas de desarrollo de Node.js, tiene una forma específica de gestionar sus operaciones. Según la documentación oficial, es un tiempo de ejecución de JavaScript asíncrono controlado por eventos. Pero ¿qué significa esto en términos más directos? Aquí, abordaré esta pregunta y exploraré las mejores prácticas a seguir al escribir código asincrónico.

¿Qué es la programación asincrónica?

La programación asincrónica significa que parte del código se puede ejecutar mientras otras partes del código esperan una respuesta. Veamos un ejemplo concreto: como desarrollador web, se ocupará extensamente de las llamadas API: escribirá código que envíe una solicitud a un servidor externo y esperará una respuesta. El problema es que, en primer lugar, ni siquiera sabes si obtendrás la respuesta que esperas, tampoco sabes cuánto tiempo tardará en ejecutarse esa función, la situación del servidor al que estás llamando, etcétera. .

Por lo tanto, cuando realiza una solicitud de llamada API, está completamente a merced del estado del servidor y su aplicación debe detener lo que esté haciendo y esperar la respuesta, al igual que su usuario espera pacientemente a que se cargue la página. . ¿Bien? Bueno, obviamente no. En el desarrollo web moderno, se cuentan incluso los milisegundos. Necesitamos una manera de hacer que nuestra aplicación se ejecute sin problemas. En otras palabras, cuando enviamos una solicitud a una API, deberíamos poder hacer algo más mientras esperamos la respuesta. Ahí es donde entra en juego la programación asincrónica.

Hay varios beneficios de la programación asincrónica. En el momento en que explore el mundo de la programación asincrónica en NodeJS, escuchará términos como bucle de eventos, funciones de devolución de llamada, promesas, escalabilidad, no bloqueo, etc. Todos estos términos giran en torno a la idea de programación asincrónica, la capacidad de ejecutar código mientras otras partes del código esperan una respuesta.

En el ejemplo anterior, estábamos realizando una llamada API hipotética a un servidor externo. Debido a que NodeJS no es bloqueante, lo que significa que nuestro código puede continuar con otras operaciones mientras la operación anterior espera una respuesta. Esto hace que NodeJS sea muy escalable porque puede manejar muchas solicitudes simultáneas. El concepto más importante que hay que entender es que en la programación asincrónica, en términos sencillos, se pueden realizar algunas operaciones mientras otras ya se están ejecutando. Veamos esto en acción.

Ciclo de eventos

En NodeJs, las operaciones se manejan en un solo subproceso . Esto significa que sólo se puede realizar una operación a la vez. Pero como vimos anteriormente, todavía podemos hacer algo mientras se procesa otra cosa.

¿Como eso es posible? NodeJS tiene un mecanismo llamado bucle de eventos que le permite a NodeJS realizar operaciones de E/S (entrada/salida) sin bloqueo, como un bucle que verifica la cola de eventos y ejecuta las operaciones en orden. Si visita la documentación oficial de NodeJS, aprenderá que el bucle de eventos consta de las siguientes fases: temporizadores, devoluciones de llamada pendientes, funciones de suspensión/preparación, sondeo, verificación y cierre de devolución de llamada.

En el mismo enlace se proporciona un breve resumen de estas fases como tal:

    temporizadores: esta fase ejecuta devoluciones de llamada programadas por setTimeout y setInterval. 
Devoluciones de llamada pendientes: ejecuta devoluciones de llamada de E/S diferidas hasta la siguiente iteración del bucle.
 inactivo, preparar: sólo se utiliza internamente.
 sondeo: recupera nuevos eventos de E/S; ejecutar devoluciones de llamadas relacionadas con E/S (casi todas con la excepción de las devoluciones de llamadas cerradas, las programadas por temporizadores y setImmediate); El nodo se bloqueará aquí cuando sea apropiado.
 comprobar: aquí se invocan las devoluciones de llamada setImmediate.
 cerrar devoluciones de llamada: algunas devoluciones de llamada cerradas, por ejemplo, socket.on('close', ...).

Aunque el bucle de eventos es un tema bastante complejo que requeriría una publicación completa por sí solo, lo más importante que hay que entender es que el bucle de eventos es un mecanismo que permite a NodeJS realizar operaciones de E/S sin bloqueo. Esto significa que NodeJS puede manejar muchas solicitudes simultáneas y por eso es tan escalable. Veamos un ejemplo de cómo funciona el bucle de eventos.

Dado que todos los navegadores modernos tienen un motor Javascript, podemos usar la consola del navegador para ver cómo funciona el bucle de eventos. Ahora bien, ¿qué pasaría si abriésemos las herramientas de desarrollo de nuestro navegador preferido y escribiéramos lo siguiente?

 console.log("uno");
 console.log("dos");
 console.log("tres");

El navegador los registrará en orden, como era de esperar: uno, dos y luego tres. Pero consideremos otro ejemplo:

 console.log("uno");
 establecerTiempo de espera( => {
  console.log("dos");
 }, 1000);
 console.log("tres");

Aquí sucederá algo diferente. Debido al método setTimeout, el navegador registrará uno, tres y, apenas un segundo después, dos. Mira, la función setTimeout es asíncrona aquí. Si bien el código sigue la cola de arriba a abajo, cuando encuentra la función setTimeout, no se queda quieto esperando ni un segundo para ejecutar el código. No, continuará su ejecución y solo después de completarla verificará la cola de eventos y verá si hay devoluciones de llamada para ejecutar.

En este caso, la función setTimeout se ejecutará después de un segundo. Este es el ejemplo más básico de cómo funciona el bucle de eventos . Este es el mismo mecanismo que permite a NodeJS manejar muchas solicitudes simultáneas, lo que lo hace muy escalable y rápido.

Ahora que hemos discutido el bucle de eventos, abordando la escalabilidad y la naturaleza sin bloqueo de NodeJS, cubramos los otros términos comunes que describimos anteriormente, como devoluciones de llamada, promesas y más.

Función de devolución de llamada

Las funciones de devolución de llamada en NodeJS son funciones que se pasan como argumentos a otras funciones y se llaman una vez que se completa la función principal. Uno de los ejemplos más básicos que puedes encontrar son los tiempos de espera. Veamos un ejemplo:

 // Función de ejemplo que realiza una operación asincrónica
 función recuperar datos (devolución de llamada) {
 // Simular un retraso
  establecerTiempo de espera( => {
    datos constantes = {nombre: "John", edad: 30};
 // Llama a la función de devolución de llamada con los datos obtenidos
    devolución de llamada (datos);
  }, 3000);
 }
 
// Llama a la función fetchData y pasa una función de devolución de llamada
 buscarDatos((datos) => {
 consola.log(datos); // { nombre: "Juan", edad: 30 }
 });

Dado que NodeJS es solo Javascript, podemos usar este mismo concepto en la consola del navegador para comprender fácilmente cómo funcionan las devoluciones de llamada. Entonces, si pega este código en la consola del navegador, los datos se recuperarán después de 3 segundos. Mientras se obtienen los datos, se ejecutará el resto del código. Una vez que se obtienen los datos, se llamará a la función de devolución de llamada y los datos se imprimirán en la consola.

La razón por la que proporcionamos una función setTimeout es para simular una llamada API, por ejemplo, y como no sabemos cuánto tiempo tardará la API en regresar, debemos simular este retraso. Pero las devoluciones de llamadas pueden salirse de control. Es posible que hayas oído hablar del término infierno de devolución de llamada. Veamos qué significa esto.

Infierno de devolución de llamada

Infierno de devolución de llamada es un término utilizado para describir una situación en la que hay demasiadas devoluciones de llamada anidadas. Incluso existe un sitio web llamado callbackhell.com sólo para explicar este concepto. Si visita esta página, se le solicitará el siguiente ejemplo:

 fs.readdir(fuente, función (err, archivos) {
  si (errar) {
    console.log("Error al encontrar archivos: " + err);
  } demás {
    archivos.forEach(función (nombre de archivo, índice de archivo) {
  console.log(nombre de archivo);
  gm(fuente + nombre de archivo).size(función (err, valores) {
    si (errar) {
      console.log("Error al identificar el tamaño del archivo: " + err); 
} demás {
 console.log(nombre de archivo + ":" + valores);
 aspecto = valores.ancho / valores.alto;
 anchos.forEach(
 función (ancho, índice de ancho) {
 altura = Math.round(ancho/aspecto);
 consola.log(
 "cambiar tamaño " + nombre de archivo + "a " + altura + "x" + altura
 );
 this.resize(ancho, alto).write(
 destino + "w" + ancho + "_" + nombre de archivo,
 función (errar) {
 if (err) console.log("Error al escribir el archivo: " + err);
 }
 );
 }.vincular(esto)
 );
 }
 });
 });
 }
 });

Son muchas devoluciones de llamadas anidadas. Como puede ver, ni siquiera es tan legible. Nos gustaría evitar estas cosas en nuestro código por nuestro propio bien.

Promesas

Ahora bien, dado que la asincronicidad es un concepto tan importante en NodeJS, hay algunas formas de abordarlo. Si bien una de ellas son las devoluciones de llamada, otra opción es utilizar promesas. Reescribamos el mismo ejemplo de setTimeout anterior, pero con promesas :

 // Función de ejemplo que devuelve una Promesa
 función buscar datos {
  devolver nueva Promesa((resolver, rechazar) => {
 // Simular un retraso
    establecerTiempo de espera( => {
  datos constantes = {nombre: "John", edad: 30};
 // Resuelve la Promesa con los datos obtenidos
  resolver(fecha);
    }, 1000);
  });
 }

 // Llama a la función fetchData y maneja el resultado de la Promesa
 obtener datos
  .entonces((datos) => { 
consola.log(datos); // { nombre: "Juan", edad: 30 }
 })
 .catch((error) => {
 consola.error(error);
 });

De esta manera se ve un poco más limpio. Al menos podemos hacer frente a posibles errores. Recuerde que no tenemos idea en qué estado está el servidor al que le estamos realizando la solicitud. No hay garantía de que nuestra solicitud obtenga la respuesta que esperamos. Por lo tanto, necesitamos una forma de manejar posibles errores. La declaración de captura aquí es una buena manera de hacer esto. Pero aquí también hay una advertencia: tal vez ya no estemos en el infierno de las devoluciones de llamadas, pero podemos caer fácilmente en las profundidades de otro infierno que se llama infierno de promesas o “infierno luego”. Aquí, "entonces infierno" recibe su nombre por tener muchas declaraciones luego encadenadas en el código. Veamos qué significa esto.

Promesa del infierno

El infierno de promesas es un término utilizado para describir una situación en la que hay demasiadas promesas anidadas. Veamos un ejemplo:

 obtener datos
  .entonces(función (datos) {
    proceso de devoluciónDatos(datos)
  .entonces(función (datosprocesados) {
    devolver datos guardados (datos procesados)
      .entonces(función (datosguardados) {
        devolver datos de visualización (datos guardados);
      })
      .catch(función (err) {
        console.log("Error al guardar datos:", err);
      });
  })
  .catch(función (err) {
    console.log("Error al procesar datos:", err);
  });
  })
  .catch(función (err) {
    console.log("Error al obtener datos:", err);
  });

Esto no es mejor que las devoluciones de llamada. ¿Entonces, cuál es la solución? Una forma es utilizar async/await. Vea cómo funciona esto.

Funciones asincrónicas (espera)

Async/await es una forma de manejar código asincrónico de forma sincrónica. Es, con diferencia, mi favorito y ayuda a evitar los peligros de una función de devolución de llamada y promesas. Veamos cómo funciona esto:

 función asíncrona recuperar datos {
  intentar {
    const res = esperar a buscar("
    datos constantes = esperar res.json;
    consola.log(datos);
    datos de devolución;
  } captura (error) {
    consola.error(error);
  }
 }

Aquí, en lugar de setTimeout, estamos realizando una solicitud de API real. Como referencia, jsonplaceholder es una API ficticia a la que podemos llamar para probar solicitudes de API. La solicitud anterior debería devolver una lista de usuarios. Ahora bien, este método de programación asincrónica funciona de la siguiente manera: la función debe llamarse async para que podamos usar la palabra clave await.

La palabra clave await es el momento en que esperamos a que finalice la operación asincrónica. Una vez que haya terminado, podemos continuar con el resto de nuestro código. También verás que usamos declaraciones try/catch, que son extremadamente útiles en este caso específico: podemos manejar los errores mucho más fácilmente gracias a este método.

Trabajar con código asíncrono en NodeJS

Adoptar el poder de la programación asincrónica en NodeJS no solo acelera nuestro código sino que también mejora significativamente el rendimiento y la capacidad de respuesta de nuestras aplicaciones.

Crear un servidor Express básico

Ahora veamos cómo podemos usar la programación asincrónica en NodeJS con un ejemplo. Crearemos un servidor express básico y enviaremos una solicitud de obtención a la API jsonplaceholder. Usaremos el método async/await para esto ya que es la forma más conveniente. Para continuar, necesitará nodo y npm instalados en su sistema.

Primero, creamos una nueva carpeta llamada 'async-node' y la ingresamos. Tenga en cuenta que estamos usando la terminal de Linux, por lo que usamos el comando mkdir para crear la carpeta. Una vez que estoy en la carpeta, ejecuto npm init -y para inicializar un nuevo proyecto npm.

La bandera -y se usa para omitir las preguntas que nos hace npm. Una vez hecho esto, instalaré los paquetes express y axios a través de npm. También usaré nodemon en este proyecto, pero no lo instalaré porque lo tengo instalado globalmente. Para facilitar su uso, te recomiendo que hagas lo mismo. Si no lo desea, también puede instalarlo localmente como una dependencia de desarrollo.

Para instalar express y axios, ejecutamos el siguiente comando:

npm expreso axios

Para nodemon, si quisiera instalarlo globalmente, escribiría npm i -g nodemon. También tenga en cuenta que aquí es posible que deba proporcionar acceso de root especificando sudo. Si quisiera instalar nodemon localmente como una dependencia de desarrollo, ejecutaría npm i –save-dev nodemon

Ahora que tengo los paquetes instalados, puedo crear un archivo index.js con el comando touch index.js y comenzar a codificar. Abriré el proyecto con VsCode mediante código. dominio. Una vez que esté allí, iré al archivo package.json y haré algunos cambios allí. Agregaré la siguiente línea a la sección de scripts:

 "guiones": {
    "test": "echo \"Error: no se ha especificado ninguna prueba\" && salida 1",
    "inicio": "nodo index.js",
    "dev": "nodemon index.js"
  },

Ahora, cada vez que ejecutamos npm run start o npm run dev, ejecutará el archivo index.js con los paquetes especificados. Ahora que hemos terminado con el archivo package.json, vayamos al archivo index.js y rellenémoslo con un servidor Express básico:

 const expreso = requerir("expreso");

 aplicación constante = expreso;

 aplicación.get(" (req, res) => {
  res.send("¡Hola mundo!");
 });

 aplicación.escuchar(3000, => {
  console.log("Servidor escuchando en el puerto 3000");
 });

Aquí están pasando algunas cosas. En primer lugar, estoy importando el paquete express y lo inicializo mediante const app = express;. A continuación, creamos una solicitud de obtención para la ruta raíz y enviamos una respuesta con el texto "¡Hola, mundo!". Finalmente, estamos iniciando el servidor en el puerto 3000. Ahora, si ejecuto npm run dev y voy a localhost:3000, si todo está correcto, deberíamos ver el texto "¡Hola, mundo!".

Enviar solicitud con Axios y escribir respuesta en un archivo

Pero queremos hacer más que simplemente crear un servidor básico. Queremos enviar una solicitud de obtención a una API externa y escribirla en un archivo. Así que hagamos esto. Compruebe el siguiente fragmento de código:

 const axios = requerir("axios");
 const fs = requerir("fs");
 const expreso = requerir("expreso");

 aplicación constante = expreso;

 app.get(" async (req, res) => {
  intentar {
 // Realizar una llamada API usando Axios
    respuesta constante = esperar axios.get (
  "
    );
    depurador;

 // Escribir datos en un archivo usando el método del sistema de archivos asíncrono
    espere fs.promises.writeFile("response.txt", JSON.stringify(response.data));

    res.send("¡Datos escritos en el archivo correctamente!");
  } captura (error) {
    consola.error(error);
    res.status(500).send("Se produjo un error");
  }
 });

 app.listen(3000, => console.log("Escuchando en el puerto 3000"));

En este código, importamos Axios y fs junto con express. Axios nos permite realizar solicitudes API, mientras que fs nos da acceso al sistema de archivos para leer y escribir archivos.

Después de inicializar la aplicación Express, agregamos un controlador de ruta GET para la ruta raíz. Tenga en cuenta la palabra clave async: esto significa que la ruta contiene código asincrónico.

Dentro del controlador, utilizamos un bloque try/catch para manejar con elegancia cualquier error. En la parte de prueba, realizamos una solicitud GET a la API ficticia usando axios y esperamos una llamada a fs.promises para crear un nuevo archivo llamado respuesta.txt. Este archivo contendrá los datos de respuesta de la API.

Cuando ejecutamos el servidor con npm run dev y visitamos localhost:3000, esperamos ver el mensaje "¡Datos escritos en el archivo correctamente!". mensaje. Pero en lugar de eso recibimos un error.

Claramente hay un error que impide que el archivo se escriba correctamente. Ahora es el momento de depurar nuestro código asincrónico para descubrir y solucionar el problema. Usaremos el depurador integrado de Node junto con otras técnicas para rastrear el flujo de ejecución e identificar problemas.

La depuración de código asincrónico como este requiere algunas habilidades especializadas, pero aprender estas habilidades nos permitirá crear aplicaciones de Nodo complejas con confianza.

Depuración de Nodejs con Chrome Dev Tools

La depuración es una habilidad fundamental para los desarrolladores. Cuando aparecen errores, necesitamos herramientas para identificar la causa raíz. En este ejemplo, nuestro código encuentra un error en lugar de escribir datos como se esperaba. Afortunadamente, Node.js tiene un depurador incorporado que podemos aprovechar.

Para usarlo, primero cierre el servidor y ejecute nodemon --inspect index.js . Esto reinicia el servidor en modo de inspección. Luego abra Chrome y busque el panel del depurador de Node usando el ícono verde de Node.js.

En la pestaña Fuentes, vemos la fila 10 resaltada donde se realiza nuestra solicitud de Axios. Esto indica que el error ocurre aquí. Podemos pausar la ejecución en la línea 10 e inspeccionar las variables para obtener más contexto.

Al verificar el punto final de la API, identificamos el problema: hay una ruta "/todos/-1" no válida. Corrijo esto a "/all/1" y reanudo la ejecución. Ahora en el depurador vemos un código de estado 200 en la solicitud: ¡éxito!

El depurador fue invaluable para rastrear el flujo de ejecución e identificar la causa raíz. Ahora con el error solucionado, reiniciamos normalmente con npm run dev . Al visitar localhost, obtenemos correctamente el mensaje "Datos escritos" y un archivo Response.txt que contiene los datos de la API.

Ser capaz de depurar código de nodo asincrónico es crucial para cualquier desarrollador. Dominar el depurador integrado y otros flujos de trabajo de depuración le brindará las habilidades para eliminar errores de manera eficiente en sus aplicaciones.

Conclusión

Aquí discutimos la programación asincrónica en NodeJs. Hemos cubierto algunas formas de lidiar con la asincronicidad, como promesas, funciones de devolución de llamada y el relativamente nuevo bloque async/await. También vimos cómo depurar nuestro código usando el depurador integrado de nodeJS. Ahora depende de usted incorporar y escribir código asincrónico en sus proyectos futuros. Sin embargo, si necesita más ayuda o desea ampliar su proyecto, considere contratar una empresa de desarrollo de Node.js de buena reputación.

Preguntas frecuentes

¿Cuál es la diferencia entre programación asincrónica y sincrónica?

La programación síncrona es un enfoque lineal en el que las tareas se ejecutan una tras otra, lo que significa que una tarea debe completarse antes de que pueda comenzar la siguiente. Esto puede hacer que su programa sea más fácil de entender, pero también significa que puede bloquearse o dejar de responder si una tarea tarda demasiado en completarse.

Una función asincrónica, por otro lado, permite ejecutar tareas simultáneamente. Si una tarea tarda demasiado en completarse (como leer un archivo de un disco o recuperar datos de la red), el programa puede continuar con otras tareas. Una vez completada la tarea larga, normalmente se invoca una función de devolución de llamada para manejar el resultado. La naturaleza sin bloqueo de las operaciones asincrónicas las hace particularmente adecuadas para realizar tareas dentro del código JavaScript que requieren esperar recursos externos o ejecutarse en segundo plano. Este enfoque puede conducir a la creación de programas potencialmente más eficientes y receptivos.

¿Cómo funciona el bucle de eventos en Node.js para manejar operaciones asincrónicas?

El bucle de eventos en Node.js es el mecanismo que maneja las operaciones asincrónicas. Cuando contratas desarrolladores de Node.js, estos aprovechan el bucle de eventos iniciando tareas asincrónicas como leer archivos o realizar solicitudes de bases de datos. Estas tareas se ejecutan fuera del bucle de eventos, lo que le permite continuar procesando otro código JavaScript. Cuando se completa una tarea asincrónica, su función de devolución de llamada se agrega a una cola o "cola de devolución de llamada". El bucle de eventos verifica esta cola y procesa las devoluciones de llamada una por una, manejando así los resultados de las operaciones asincrónicas de manera eficiente.

Fuente: BairesDev

Regresar al blog

Deja un comentario

Ten en cuenta que los comentarios deben aprobarse antes de que se publiquen.