Node.js

Node

Tutorial Node: Fundamentos del entorno Node.js

Descubre cómo el event loop de Node.js revoluciona la programación asincrónica, gestionando múltiples tareas sin bloquear ejecución.

Aprende Node GRATIS y certifícate

Cómo funciona el Event Loop en Node.js y su importancia

El event loop es el mecanismo fundamental que permite a Node.js manejar operaciones asíncronas. A diferencia de los entornos tradicionales, donde cada petición podría bloquear un hilo, Node.js utiliza un único hilo para gestionar múltiples tareas sin bloquear la ejecución.

En Node.js, cuando se realiza una operación de entrada/salida (E/S), el sistema delega esa tarea al sistema operativo, permitiendo que el hilo principal continúe ejecutando otras operaciones. Una vez que la operación E/S finaliza, el event loop se encarga de ejecutar la función de devolución de llamada (callback) asociada.

El funcionamiento interno del event loop se divide en varias fases, las cuales se repiten continuamente:

  1. Timers: Ejecución de callbacks programados con setTimeout y setInterval.
  2. Pending Callbacks: Ejecución de callbacks de algunas operaciones del sistema.
  3. Idle, Prepare: Operaciones internas para Node.js.
  4. Poll: Recuperación de nuevas E/S; aquí es donde el event loop pasa la mayor parte del tiempo.
  5. Check: Ejecución de callbacks de setImmediate.
  6. Close Callbacks: Manejo de eventos de cierre, como socket.on('close', ...).

Un ejemplo ilustrativo es el siguiente código:

console.log('Inicio');

setTimeout(() => {
  console.log('Timeout 0 ms');
}, 0);

setImmediate(() => {
  console.log('Immediate');
});

console.log('Fin');

El resultado de este script será:

Inicio
Fin
Immediate
Timeout 0 ms

Aunque pueda parecer contraintuitivo, el callback de setImmediate se ejecuta antes que el de setTimeout con 0 milisegundos. Esto se debe a que setImmediate se encola en la fase check del event loop, mientras que setTimeout se encola en la fase timers, y el temporizador inicia después de completar la fase actual.

Es crucial comprender que las funciones asíncronas no bloquean el hilo de ejecución. Esto permite que Node.js maneje múltiples conexiones simultáneas con alto rendimiento. Sin embargo, si se ejecuta una operación sincrónica pesada, como una función de cálculo intensivo, se bloqueará el event loop y afectará negativamente a la escalabilidad de la aplicación.

Para mitigar este problema, Node.js introdujo los Worker Threads, que permiten ejecutar operaciones de CPU intensivas en hilos separados:

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  // Este bloque se ejecuta en el hilo principal
  const worker = new Worker(__filename);
  worker.on('message', (resultado) => {
    console.log(`Resultado: ${resultado}`);
  });
  worker.on('error', (error) => {
    console.error(`Error del worker: ${error}`);
  });
  worker.on('exit', (code) => {
    if (code !== 0) {
      console.error(`Worker finalizó con código de salida ${code}`);
    }
  });
} else {
  // Este bloque se ejecuta en el worker
  const calcularPrimos = (limite) => {
    const primos = [];
    for (let i = 2; i <= limite; i++) {
      let esPrimo = true;
      for (let j = 2; j < i; j++) {
        if (i % j === 0) {
          esPrimo = false;
          break;
        }
      }
      if (esPrimo) {
        primos.push(i);
      }
    }
    return primos;
  };

  const resultado = calcularPrimos(100000);
  parentPort.postMessage(resultado);
}

Con esta estructura, las operaciones pesadas no bloquean el event loop principal, manteniendo la aplicación responsiva.

Manejo de require, import y export en Node.js

En Node.js, los módulos son fundamentales para organizar y reutilizar el código. Un módulo es simplemente un archivo que contiene código de JavaScript que puede ser exportado y luego importado en otros archivos, permitiendo una estructura más mantenible y modular en las aplicaciones.

Existen dos tipos principales de módulos en Node.js: los módulos internos y los módulos externos. Los módulos internos, también conocidos como módulos nativos o incorporados, son parte del núcleo de Node.js y proporcionan funcionalidades esenciales sin necesidad de instalación adicional. Ejemplos de estos son fs para operaciones de archivos, http para crear servidores web y path para manipular rutas de archivos.

Por otro lado, los módulos externos son paquetes desarrollados por la comunidad que se pueden instalar mediante herramientas como npm o yarn. Estos módulos amplían las capacidades de Node.js y permiten integrar funcionalidades adicionales. Para utilizarlos, es necesario instalarlos previamente en el proyecto utilizando comandos como npm install.

Históricamente, Node.js ha utilizado el sistema de módulos CommonJS, que hace uso de require para importar módulos y module.exports o exports para exportarlos. Un ejemplo básico de uso de require con un módulo interno sería:

const fs = require('fs');

fs.readFile('/ruta/al/archivo.txt', 'utf8', (err, datos) => {
  if (err) throw err;
  console.log(datos);
});

Para exportar funcionalidades en CommonJS, se utiliza module.exports. Supongamos que tenemos un archivo matematicas.js:

function suma(a, b) {
  return a + b;
}

module.exports = { suma };

Luego, en otro archivo, podemos importar y utilizar esta función:

const { suma } = require('./matematicas');

console.log(suma(2, 3)); // Salida: 5

Con la evolución de JavaScript, se introdujeron los módulos ES6 (ES Modules), que utilizan las palabras clave import y export. Node.js, a partir de versiones recientes, soporta de forma nativa los módulos ES6, permitiendo escribir código más alineado con los estándares modernos.

Para utilizar exportaciones en ES Modules, se puede hacer de la siguiente manera:

// matematicas.mjs
export function suma(a, b) {
  return a + b;
}

Y para importar:

// app.mjs
import { suma } from './matematicas.mjs';

console.log(suma(2, 3)); // Salida: 5

Es importante destacar que, por defecto, Node.js trata los archivos con extensión .js como módulos CommonJS. Para indicar que un proyecto utiliza ES Modules, se debe añadir el campo "type": "module" en el archivo package.json. Alternativamente, se pueden utilizar extensiones .mjs para archivos de módulos ES6.

La diferencia entre require y import radica en el sistema de módulos que utilizan. require es propio de CommonJS, mientras que import pertenece a los módulos ES6. Aunque cumplen funciones similares, tienen diferencias en su comportamiento. Por ejemplo, import es estático y se procesa en tiempo de compilación, mientras que require es dinámico y se ejecuta en tiempo de ejecución.

Al decidir entre utilizar CommonJS o ES Modules, es recomendable optar por ES Modules si se está desarrollando un nuevo proyecto, ya que es el estándar actual y facilita la interoperabilidad con herramientas modernas. Sin embargo, si se trabaja en proyectos existentes que utilizan CommonJS o se requiere compatibilidad con módulos que aún no soportan ES Modules, es aceptable continuar con require y module.exports.

Al importar módulos externos instalados mediante npm, el proceso es similar. Por ejemplo, si instalamos el paquete axios para realizar peticiones HTTP, podemos importarlo de la siguiente manera:

Usando CommonJS:

const axios = require('axios');

axios.get('https://api.ejemplo.com/datos')
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });

Usando ES Modules:

import axios from 'axios';

try {
  const response = await axios.get('https://api.ejemplo.com/datos');
  console.log(response.data);
} catch (error) {
  console.error(error);
}

Es posible utilizar import() como una función dinámica para cargar módulos de forma asíncrona. Esto es útil cuando se desea cargar módulos condicionalmente o reducir el tiempo de carga inicial. Por ejemplo:

if (condicion) {
  const modulo = await import('./moduloEspecial.mjs');
  modulo.funcionEspecial();
}

Al exportar módulos, también se pueden utilizar exportaciones por defecto (default exports) y exportaciones nombradas. Las exportaciones por defecto permiten exportar un único valor predeterminado, mientras que las exportaciones nombradas permiten exportar múltiples valores. Ejemplo de exportación por defecto:

// utilidades.mjs
export default function saludar(nombre) {
  console.log(`Hola, ${nombre}`);
}

E importación:

// app.mjs
import saludar from './utilidades.mjs';

saludar('Carlos'); // Salida: Hola, Carlos

Al combinar exportaciones nombradas y por defecto, se puede ofrecer una API más flexible en los módulos. Es importante mantener consistencia en el estilo y ser consciente de cómo se estructuran las exportaciones para evitar confusiones al importar.

Es importante considerar que algunos módulos externos pueden no ser compatibles con ES Modules. En esos casos, se pueden utilizar herramientas como Babel para transpilar el código o emplear soluciones híbridas que permitan interoperabilidad entre CommonJS y ES Modules.

Diferencias entre global y window en entornos de ejecución

En el mundo de JavaScript, el contexto global es un concepto fundamental que varía dependiendo del entorno de ejecución. En los navegadores web, el objeto global se conoce como window, mientras que en Node.js es global. Comprender las diferencias entre global y window es crucial para desarrollar aplicaciones que funcionen correctamente en el servidor y en el navegador.

En el navegador, el objeto window representa la ventana del navegador y actúa como el objeto global del entorno. Todas las variables y funciones declaradas en el ámbito global se convierten en propiedades de window. Por ejemplo:

// En el navegador
var nombre = 'Ana';
function saludar() {
  console.log('Hola');
}

console.log(window.nombre); // 'Ana'
window.saludar(); // 'Hola'

En este caso, tanto nombre como saludar se añaden al objeto window. Esto permite acceder a ellas desde cualquier parte del código que tenga acceso al contexto global del navegador.

Por otro lado, en Node.js, el objeto global se denomina global. Sin embargo, a diferencia del navegador, las variables y funciones definidas en el ámbito global de un módulo no se agregan automáticamente al objeto global. Esto se debe a que Node.js utiliza el sistema de módulos CommonJS, donde cada archivo es un módulo independiente con su propio ámbito. Por ejemplo:

// En Node.js
var nombre = 'Carlos';
function saludar() {
  console.log('Hola');
}

console.log(global.nombre); // undefined
global.saludar(); // TypeError: global.saludar is not a function

En este ejemplo, nombre y saludar no están disponibles en el objeto global. Para hacer que una variable o función esté disponible globalmente en Node.js, es necesario asignarla explícitamente al objeto global:

global.nombre = 'Lucía';
global.saludar = function() {
  console.log('Hola');
};

console.log(global.nombre); // 'Lucía'
global.saludar(); // 'Hola'

Es importante destacar que agregar propiedades al objeto global en Node.js no es una práctica recomendada, ya que puede generar conflictos y dificultar el mantenimiento del código. En su lugar, es preferible utilizar módulos y exportaciones para compartir código entre diferentes partes de la aplicación.

Además, en el contexto de Node.js, existe el objeto process, que proporciona información y control sobre el proceso en ejecución. Por ejemplo, process.env permite acceder a las variables de entorno, y process.argv contiene los argumentos pasados al script:

console.log(process.env.PATH); // Muestra la variable de entorno PATH
console.log(process.argv); // Muestra los argumentos del script

Otra diferencia clave es que en el navegador, ciertas APIs como el DOM y funciones como alert() o confirm() están disponibles a través del objeto window, mientras que en Node.js, dichas APIs no existen. Node.js proporciona sus propias APIs para interactuar con el sistema operativo, como el módulo fs para manipular el sistema de archivos:

const fs = require('fs');

fs.readFile('/ruta/al/archivo.txt', 'utf8', (err, datos) => {
  if (err) throw err;
  console.log(datos);
});

Además, en los navegadores modernos, el objeto window también actúa como el objeto global para las APIs de fetch y WebSockets, permitiendo realizar peticiones HTTP y comunicación en tiempo real. En Node.js, aunque inicialmente estas APIs no estaban disponibles, a partir de versiones recientes, se ha incorporado de forma nativa el soporte para fetch:

// En Node.js v23.3.0
const respuesta = await fetch('https://api.ejemplo.com/datos');
const datos = await respuesta.json();
console.log(datos);

Es crucial tener en cuenta que el uso de var para declarar variables globales en el navegador y en Node.js tiene comportamientos distintos. En el navegador, como se mencionó, las variables globales declaradas con var se añaden al objeto window. Sin embargo, en Node.js, aunque se utilice var, las variables se mantienen dentro del ámbito del módulo. Para evitar problemas de ámbito y promocionar buenas prácticas, es recomendable utilizar const y let para declarar variables, limitando su alcance al bloque donde se definen.

Además, Node.js introduce el objeto globalThis que es un estándar de JavaScript para referirse al objeto global en cualquier entorno. Tanto en el navegador como en Node.js, globalThis apunta al objeto global correspondiente:

// Funciona en ambos entornos
globalThis.miVariableGlobal = 'Valor global';

console.log(globalThis.miVariableGlobal); // 'Valor global'

El uso de globalThis proporciona una forma unificada de acceder al objeto global sin preocuparse por el entorno de ejecución, lo que mejora la portabilidad del código entre el servidor y el navegador.

Sin embargo, es importante ser cauteloso al utilizar el objeto global, ya que manipular el espacio de nombres global puede llevar a conflictos y errores difíciles de depurar. En entornos complejos, es preferible encapsular el código y utilizar patrones como módulos o clases para mantener un código limpio y modular.

Asincronía 101: callbacks, promesas y async/await en Node.js

La asincronía es fundamental en Node.js, permitiendo manejar operaciones de E/S sin bloquear el event loop. Para gestionar estas operaciones, Node.js ofrece diversas técnicas: callbacks, promesas y async/await, cada una aportando diferentes ventajas en la escritura y legibilidad del código.

Un callback es una función pasada como argumento a otra función, que se ejecuta después de que una operación asíncrona ha completado. Este patrón ha sido ampliamente utilizado desde los inicios de Node.js. Por ejemplo:

const fs = require('fs');

fs.readFile('archivo.txt', 'utf8', (err, datos) => {
  if (err) {
    console.error('Error al leer el archivo:', err);
    return;
  }
  console.log('Contenido del archivo:', datos);
});

En este ejemplo, la función de callback maneja tanto el error como el resultado. Sin embargo, el uso intensivo de callbacks puede conducir al llamado "infierno de callbacks" o callback hell, donde la anidación excesiva dificulta la lectura y mantenimiento del código.

Para abordar este problema, se introdujeron las promesas, una abstracción que permite manejar operaciones asíncronas de manera más estructurada. Una promesa representa un resultado eventual de una operación asíncrona y puede estar en uno de tres estados: pendiente, cumplida o rechazada. El código con promesas mejora la legibilidad:

const fs = require('fs').promises;

fs.readFile('archivo.txt', 'utf8')
  .then((datos) => {
    console.log('Contenido del archivo:', datos);
  })
  .catch((err) => {
    console.error('Error al leer el archivo:', err);
  });

Al utilizar promesas, se evita la anidación profunda de callbacks y se facilita el manejo de errores. Además, es posible encadenar múltiples promesas para ejecutar operaciones secuencialmente o en paralelo.

La llegada de async/await simplificó aún más el código asíncrono, permitiendo escribirlo de manera similar al código sincrónico. Para utilizar await, la función que lo contiene debe declararse con async. Por ejemplo:

const fs = require('fs').promises;

async function leerArchivo() {
  try {
    const datos = await fs.readFile('archivo.txt', 'utf8');
    console.log('Contenido del archivo:', datos);
  } catch (err) {
    console.error('Error al leer el archivo:', err);
  }
}

leerArchivo();

Con async/await, se puede manejar la asincronía sin necesidad de encadenar métodos .then() y .catch(), utilizando en su lugar bloques try...catch para el manejo de errores.

Para ejecutar operaciones asíncronas en paralelo, se puede emplear Promise.all junto con await:

async function procesarArchivos() {
  try {
    const [datos1, datos2] = await Promise.all([
      fs.readFile('archivo1.txt', 'utf8'),
      fs.readFile('archivo2.txt', 'utf8')
    ]);
    console.log('Archivo 1:', datos1);
    console.log('Archivo 2:', datos2);
  } catch (err) {
    console.error('Error al leer los archivos:', err);
  }
}

procesarArchivos();

En este caso, ambas operaciones se inician simultáneamente, mejorando la eficiencia.

Es importante manejar correctamente los errores en operaciones asíncronas. Con async/await, los errores se capturan mediante try...catch, lo que permite un manejo más claro y centralizado. Los errores no capturados pueden provocar comportamientos inesperados o la terminación abrupta del proceso de Node.js.

Además de utilizar funciones que ya devuelven promesas, es posible crear promesas personalizadas:

function esperar(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function ejecutar() {
  console.log('Esperando 2 segundos...');
  await esperar(2000);
  console.log('Listo');
}

ejecutar();

En este ejemplo, esperar retorna una promesa que se resuelve después de un tiempo determinado, simulando una operación asíncrona.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Node GRATIS online

Todas las lecciones de Node

Accede a todas las lecciones de Node y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Node y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el funcionamiento del event loop en Node.js.
  • Distinguir las diferentes fases del event loop y su impacto.
  • Analizar el rol de callbacks en la ejecución de código asíncrono.
  • Comparar operaciones asíncronas y sincrónicas en Node.js.
  • Explorar el uso de Worker Threads para operaciones de CPU intensivas.