Funcionamiento de Node.js

Completado

En esta unidad se explica cómo Node.js controla las tareas entrantes en el entorno de ejecución de JavaScript.

Tipos de tareas

Las aplicaciones de JavaScript tienen dos tipos de tareas:

  • Tareas sincrónicas: Estas tareas se producen en orden. No dependen de otro recurso para completarse. Algunos ejemplos son operaciones matemáticas o manipulación de cadenas.
  • Asincrónicas: Es posible que estas tareas no se completen inmediatamente porque dependen de otros recursos. Algunos ejemplos son las solicitudes de red o las operaciones del sistema de archivos.

Dado que quiere que el programa se ejecute lo más rápido posible, quiere que el motor de JavaScript pueda seguir trabajando mientras espera una respuesta de una operación asincrónica. Para ello, agrega la tarea asincrónica a una cola de tareas y continúa trabajando en la siguiente tarea.

Administración de la cola de tareas con bucle de eventos

Node.js usa la arquitectura controlada por eventos del motor JavaScript para procesar solicitudes asincrónicas. En el siguiente diagrama se muestra cómo funciona el bucle de eventos V8, en general:

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

Una tarea asincrónica, indicada por la sintaxis adecuada (mostrada a continuación), se agrega al bucle de eventos. La tarea incluye el trabajo que se va a realizar y una función de devolución de llamada para recibir los resultados. Cuando se completa la operación intensiva, la función de devolución de llamada se desencadena con los resultados.

Operaciones sincrónicas frente a operaciones asincrónicas

Las API Node.js proporcionan tanto operaciones asíncronas como síncronas para algunas de las mismas operaciones, como las operaciones con archivos. Aunque por lo general siempre hay que pensar primero en asincrónico, hay ocasiones en las que puede usar operaciones sincrónicas.

Un ejemplo es cuando una interfaz de línea de comandos (CLI) lee un archivo y, a continuación, usa inmediatamente los datos del archivo. En este caso, puede usar la versión sincrónica de la operación de archivo porque no hay ningún otro sistema o persona a la espera de usar la aplicación.

Sin embargo, si va a compilar un servidor web, siempre debe usar la versión asincrónica de la operación de archivo para no bloquear la capacidad de ejecución del subproceso único para procesar otras solicitudes de usuario.

En su trabajo como desarrollador en TailWind Traders, deberá comprender la diferencia entre las operaciones sincrónicas y asincrónicas y cuándo usar cada una.

Rendimiento mediante operaciones asincrónicas

Node.js aprovecha la naturaleza única controlada por eventos de JavaScript que hace que la creación de tareas del servidor sea rápida y de alto rendimiento. JavaScript, cuando se usa correctamente con técnicas asincrónicas, puede generar los mismos resultados de rendimiento que lenguajes de bajo nivel como C debido a las mejoras de rendimiento que posibilita el motor V8.

Las técnicas asincrónicas incluyen 3 estilos, que debe poder reconocer en su trabajo:

  • Async/await (recomendado): La técnica asincrónica más reciente que usa las palabras clave async y await para recibir los resultados de una operación asincrónica. Async/await se usa en muchos lenguajes de programación. Por lo general, los nuevos proyectos con dependencias más recientes usarán este estilo de código asincrónico.
  • Devoluciones de llamada: La técnica asincrónica original que usa una función de devolución de llamada para recibir los resultados de una operación asincrónica. Verá esto en las bases de código anteriores y en las API Node.js más antiguas.
  • Promesas: Técnica asincrónica más reciente que usa un objeto de promesa para recibir los resultados de una operación asincrónica. Verá esto en las bases de código más recientes y en las API Node.js más recientes. Es posible que tenga que escribir código basado en promesas en el trabajo para encapsular las API más antiguas que no se actualizarán. Al usar promesas para este encapsulado, se permite que el código se use en un intervalo mayor de proyectos con versiones de Node.js que en el estilo de código asincrónico/await más reciente.

Async/await

Async/await es una manera más reciente de controlar la programación asincrónica. Async/await es el suplemento sintáctico basado en promesas y hace que el código asincrónico sea más similar al código sincrónico. También es más fácil de leer y mantener.

El mismo ejemplo con async/await tiene este aspecto:

// async/await asynchronous example

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

const filePath = './file.txt';

// `async` before the parent function
async function readFileAsync() {
  try {
    // `await` before the async method
    const data = await fs.readFile(filePath, 'utf-8');
    console.log(data);
    console.log('Done!');
  } catch (error) {
    console.log('An error occurred...: ', error);
  }
}

readFileAsync()
  .then(() => {
    console.log('Success!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

Cuando se publicó async/await en ES2017, las palabras clave solo podían usarse en funciones cuya función de nivel superior fuera una promesa. Aunque la promesa no tenía que tener secciones then y catch, todavía era necesario tener sintaxis promise para ejecutarse.

Una función async siempre devuelve una promesa, incluso si no tiene una llamada await dentro de ella. La promesa se resolverá con el valor devuelto por la función. Si la función produce un error, la promesa se rechazará con el valor producido.

Promesas

Dado que las devoluciones de llamada anidadas pueden ser difíciles de leer y administrar, Node.js agregó compatibilidad con promesas. Una promesa es un objeto que representa la eventual finalización (o error) de una operación asincrónica.

Una función de promesa tiene el formato siguiente:

// Create a basic promise function
function promiseFunction() {
  return new Promise((resolve, reject) => {
    // do something

    if (error) {
      // indicate success
      reject(error);
    } else {
      // indicate error
      resolve(data);
    }
  });
}

// Call a basic promise function
promiseFunction()
  .then((data) => {
    // handle success
  })
  .catch((error) => {
    // handle error
  });

Se llama al método then cuando se cumple la promesa y se llama al método catch cuando se rechaza la promesa.

Para leer un archivo de forma asincrónica con promesas, el código es el siguiente:

// promises asynchronous example

const fs = require('fs').promises;
const filePath = './file.txt';

// request to read a file
fs.readFile(filePath, 'utf-8')
  .then((data) => {
    console.log(data);
    console.log('Done!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

console.log(`I'm the last line of the file!`);

Async/await de nivel superior

Las versiones más recientes de Node.js agregaron async/await de nivel superior para los módulos ES6. Debe agregar una propiedad denominada type en package.json con un valor de module para usar esta característica.

{
    "type": "module"
}

A continuación, puede usar la palabra clave await en el nivel superior del código

// top-level async/await asynchronous example

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

const filePath = './file.txt';

// `async` before the parent function
try {
  // `await` before the async method
  const data = await fs.readFile(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");

Devoluciones de llamada

Cuando Node.js se publicó originalmente, la programación asincrónica se controlaba mediante funciones de devolución de llamada. Las devoluciones de llamada son funciones que se pasan como argumentos a otras funciones. Una vez completada la tarea, se llama a la función de devolución de llamada.

El orden de los parámetros de la función es importante. La función de devolución de llamada es el último parámetro de la función.

// Callback function is the last parameter
function(param1, param2, paramN, callback)

El nombre de la función en el código que mantiene podría no llamarse callback. Se podría llamar cb, done o next. El nombre de la función no es importante, pero el orden de los parámetros sí es importante.

Observe que no hay ninguna indicación sintáctica de que la función es asincrónica. Tiene que saber que la función es asíncrona leyendo la documentación o continuando leyendo el código.

Ejemplo de devolución de llamada con función de devolución de llamada con nombre

El código siguiente separa la función asincrónica de la devolución de llamada. Esto es fácil de leer y comprender y le permite reutilizar la devolución de llamada para otras funciones asincrónicas.

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// callback
const callback = (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
};

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);

console.log("I'm the last line of the file!");

El resultado correcto es:

I'm the last line of the file!
Hi, developers!
Done!

En primer lugar, se inicia la función fs.readFile asincrónica y entra en el bucle de eventos. A continuación, la ejecución del código continúa con la siguiente línea de código, que es la última console.log. Una vez leído el archivo, se llama a la función de devolución de llamada y se ejecutan las dos instrucciones console.log.

Ejemplo de devolución de llamada con función anónima

En el ejemplo siguiente se usa una función de devolución de llamada anónima, lo que significa que la función no tiene un nombre y no se puede reutilizar mediante otras funciones anónimas.

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
});

console.log("I'm the last line of the file!");

El resultado correcto es:

I'm the last line of the file!
Hi, developers!
Done!

Cuando se ejecuta el código, se inicia la función asincrónica fs.readFile y entra en el bucle de eventos. A continuación, la ejecución continúa con la siguiente línea de código, que es la última console.log. Cuando se lee el archivo, se llama a la función de devolución de llamada y se ejecutan las dos instrucciones console.log.

Devoluciones de llamada anidadas

Dado que es posible que tenga que llamar a una devolución de llamada asincrónica posterior y, a continuación, otra, el código de devolución de llamada podría estar anidado. Esto se denomina infierno de devolución de llamada y es difícil de leer y mantener.

// nested callback example

// file system module from Node.js
const fs = require('fs');

fs.readFile(param1, param2, (error, data) => {
  if (!error) {
    fs.writeFile(paramsWrite, (error, data) => {
      if (!error) {
        fs.readFile(paramsRead, (error, data) => {
          if (!error) {
            // do something
          }
        });
      }
    });
  }
});

API sincrónicas

Node.js también tiene un conjunto de API sincrónicas. Estas API bloquean la ejecución del programa hasta que se complete la tarea. Las API sincrónicas son útiles cuando desea leer un archivo y, a continuación, usar inmediatamente los datos del archivo.

Las funciones sincrónicas (bloqueo) de Node.js usan la convención de nomenclatura de functionSync. Por ejemplo, la API asincrónica readFile tiene un homólogo sincrónico denominado readFileSync. Es importante mantener este estándar en sus propios proyectos para que el código sea fácil de leer y comprender.

// synchronous example

const fs = require('fs');

const filePath = './file.txt';

try {
  // request to read a file
  const data = fs.readFileSync(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}

Como desarrollador nuevo en TailWind Traders, es posible que se le pida que modifique cualquier tipo de código Node.js. Es importante comprender la diferencia entre las API sincrónicas y asincrónicas y las diferentes sintaxis para el código asincrónico.