Conversión de devoluciones de llamada en promesas en Node.js

C

Introducción

Hace unos años, las devoluciones de llamada eran la única forma en que podíamos lograr la ejecución de código asincrónico en JavaScript. Hubo pocos problemas con las devoluciones de llamada y el más notable fue el “infierno de las devoluciones de llamada”.

Con ES6, Promises se introdujo como una solución a esos problemas. Y finalmente, el async/await Se introdujeron palabras clave para una experiencia aún más agradable y una mejor legibilidad.

Incluso con la adición de nuevos enfoques, todavía hay muchos módulos nativos y bibliotecas que usan devoluciones de llamada. En este artículo, hablaremos sobre cómo convertir las devoluciones de llamada de JavaScript en Promesas. El conocimiento de ES6 será útil, ya que usaremos funciones como operadores de propagación para facilitar las cosas.

¿Qué es una devolución de llamada?

Una devolución de llamada es un argumento de función que resulta ser una función en sí misma. Si bien podemos crear cualquier función para aceptar otra función, las devoluciones de llamada se utilizan principalmente en operaciones asincrónicas.

JavaScript es un lenguaje interpretado que solo puede procesar una línea de código a la vez. Algunas tareas pueden tardar bastante en completarse, como descargar o leer un archivo grande. JavaScript descarga estas tareas de larga ejecución a un proceso diferente en el navegador o el entorno de Node.js. De esa manera, no bloquea la ejecución del resto del código.

Por lo general, las funciones asincrónicas aceptan una función de devolución de llamada, de modo que cuando estén completas podamos procesar sus datos.

Tomemos un ejemplo, escribiremos una función de devolución de llamada que se ejecutará cuando el programa lea con éxito un archivo de nuestro disco duro.

Con este fin, usaremos un archivo de texto llamado sample.txt, que contiene lo siguiente:

Hello world from sample.txt

Luego, escriba un script simple de Node.js para leer el archivo:

const fs = require('fs');

fs.readFile('./sample.txt', 'utf-8', (err, data) => {
    if (err) {
        // Handle error
        console.error(err);
          return;
    }

    // Data is string do something with it
    console.log(data);
});

for (let i = 0; i < 10; i++) {
    console.log(i);
}

Ejecutar este código debería producir:

0
...
8
9
Hello world from sample.txt

Si ejecuta este código, debería ver 0..9 que se imprime antes de que se ejecute la devolución de llamada. Esto se debe a la gestión asincrónica de JavaScript de la que hemos hablado anteriormente. La devolución de llamada, que registra el contenido del archivo, solo se llamará después de que se lea el archivo.

Como nota al margen, las devoluciones de llamada también se pueden usar en métodos síncronos. Por ejemplo, Array.sort() acepta una función de devolución de llamada que le permite personalizar cómo se ordenan los elementos.

Las funciones que aceptan devoluciones de llamada se denominan funciones de orden superior.

Ahora tenemos una mejor idea de las devoluciones de llamada. Sigamos adelante y veamos qué es una Promesa.

¿Qué es una promesa?

Se introdujeron promesas con ECMAScript 2015 (comúnmente conocido como ES6) para mejorar la experiencia del desarrollador con la programación asincrónica. Como sugiere su nombre, es una promesa de que un objeto JavaScript eventualmente devolverá un valor o un error.

Una promesa tiene 3 estados:

  • Pendiente: El estado inicial que indica que la operación asincrónica no está completa.
  • Cumplido: Lo que significa que la operación asincrónica se completó correctamente.
  • Rechazado: Significa que la operación asincrónica falló.

La mayoría de las promesas terminan luciendo así:

someAsynchronousFunction()
    .then(data => {
        // After promise is fulfilled
        console.log(data);
    })
    .catch(err => {
        // If promise is rejected
        console.error(err);
    });

Las promesas son importantes en JavaScript moderno, ya que se utilizan con async/await palabras clave que se introdujeron en ECMAScript 2016. Con async/await, no necesitamos utilizar devoluciones de llamada ni then() y catch() para escribir código asincrónico.

Si se adaptara el ejemplo anterior, se vería así:

try {
    const data = await someAsynchronousFunction();
} catch(err) {
    // If promise is rejected
    console.error(err);
}

¡Esto se parece mucho a JavaScript síncrono “normal”! Puedes aprender más sobre async/await en nuestro artículo, Node.js Async Await en ES7.

Las bibliotecas JavaScript más populares y los proyectos nuevos usan Promises con la async/await palabras clave.

Sin embargo, si está actualizando un repositorio existente o encuentra una base de código heredada, probablemente le interese mover las API basadas en devolución de llamada a una API basada en Promise para mejorar su experiencia de desarrollo. Tu equipo también estará agradecido.

¡Veamos un par de métodos para convertir devoluciones de llamada en promesas!

Convertir una devolución de llamada en una promesa

Node.js Promisify

La mayoría de las funciones asincrónicas que aceptan una devolución de llamada en Node.js, como la fs (sistema de archivos), tiene un estilo de implementación estándar: la devolución de llamada se pasa como último parámetro.

Por ejemplo, así es como puede leer un archivo usando fs.readFile() sin especificar la codificación del texto:

fs.readFile('./sample.txt', (err, data) => {
    if (err) {
        console.error(err);
          return;
    }

    // Data is a buffer
    console.log(data);
});

Nota: Si especifica utf-8 como la codificación obtendrá una salida de cadena. Si no especifica la codificación, obtendrá un Buffer salida.

Además, la devolución de llamada, que se pasa a la función, debe aceptar una Error ya que es el primer parámetro. Después de eso, puede haber cualquier número de salidas.

Si la función que necesita convertir en una Promesa sigue esas reglas, puede usar util.promisify, un módulo nativo de Node.js que oculta las devoluciones de llamada a Promises.

Para hacer eso, primero importe el util módulo:

const util = require('util');

Entonces usas el promisify método para convertirlo en una promesa:

const fs = require('fs');
const readFile = util.promisify(fs.readFile);

Ahora use la función recién creada como una promesa regular:

readFile('./sample.txt', 'utf-8')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.log(err);
    });

Alternativamente, puede utilizar el async/await palabras clave como se muestra en el siguiente ejemplo:

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

(async () => {
    try {
        const content = await readFile('./sample.txt', 'utf-8');
        console.log(content);
    } catch (err) {
        console.error(err);
    }
})();

Solo puedes usar el await palabra clave dentro de una función que se creó con async, de ahí la razón por la que tenemos un contenedor de funciones en este ejemplo. Este contenedor de función también se conoce como Expresiones de función invocadas inmediatamente.

Si su devolución de llamada no sigue ese estándar en particular, no se preocupe. los util.promisify() La función puede permitirle personalizar cómo ocurre la conversión.

Nota: Las promesas se hicieron populares poco después de su presentación. Node.js ya ha convertido la mayoría, si no todas, de sus funciones principales de una devolución de llamada a una API basada en Promise.

Si necesita trabajar con archivos usando Promesas, use el biblioteca que viene con Node.js.

Hasta ahora, ha aprendido cómo convertir las devoluciones de llamada de estilo estándar de Node.js en promesas. Este módulo solo está disponible en Node.js a partir de la versión 8. Si está trabajando en el navegador o en una versión anterior de Node, probablemente sería mejor que creara su propia versión de la función basada en promesas.

Creando tu promesa

Hablemos sobre cómo ocultar devoluciones de llamada a promesas si el util.promisify() La función no está disponible.

La idea es crear un nuevo Promise objeto que envuelve la función de devolución de llamada. Si la función de devolución de llamada devuelve un error, rechazamos la Promesa con el error. Si la función de devolución de llamada devuelve una salida sin errores, resolvemos la Promesa con la salida.

Comencemos por convertir una devolución de llamada en una promesa para una función que acepta un número fijo de parámetros:

const fs = require('fs');

const readFile = (fileName, encoding) => {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, encoding, (err, data) => {
            if (err) {
                return reject(err);
            }

            resolve(data);
        });
    });
}

readFile('./sample.txt')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.log(err);
    });

Nuestra nueva función readFile() acepta los dos argumentos que hemos estado usando para leer archivos con fs.readFile(). Luego creamos un nuevo Promise objeto que envuelve la función, que acepta la devolución de llamada, en este caso, fs.readFile().

En lugar de devolver un error, reject la promesa. En lugar de registrar los datos de inmediato, resolve la promesa. Luego usamos nuestro basado en promesas readFile() funciona como antes.

Probemos con otra función que acepte un número dinámico de parámetros:

const getMaxCustom = (callback, ...args) => {
    let max = -Infinity;

    for (let i of args) {
        if (i > max) {
            max = i;
        }
    }

    callback(max);
}

getMaxCustom((max) => { console.log('Max is ' + max) }, 10, 2, 23, 1, 111, 20);

El parámetro de devolución de llamada también es el primer parámetro, lo que lo hace un poco inusual con funciones que aceptan devoluciones de llamada.

La conversión a una promesa se realiza de la misma manera. Creamos un nuevo Promise objeto que envuelve nuestra función que usa una devolución de llamada. Nosotros entonces reject si encontramos un error y resolve cuando tengamos el resultado.

Nuestra versión prometida se ve así:

const getMaxPromise = (...args) => {
    return new Promise((resolve) => {
        getMaxCustom((max) => {
            resolve(max);
        }, ...args);
    });
}

getMaxCustom(10, 2, 23, 1, 111, 20)
    .then(max => console.log(max));

Al crear nuestra promesa, no importa si la función usa devoluciones de llamada de una manera no estándar o con muchos argumentos. Tenemos el control total de cómo se hace y los principios son los mismos.

Conclusión

Si bien las devoluciones de llamada han sido la forma predeterminada de aprovechar el código asincrónico en JavaScript, las promesas son un método más moderno que los desarrolladores creen que es más fácil de usar. Si alguna vez encontramos una base de código que utiliza devoluciones de llamada, ahora podemos hacer que esa función sea una Promesa.

En este artículo, vio por primera vez cómo usar utils.promisfy() en Node.js para convertir funciones que aceptan devoluciones de llamada en promesas. Luego viste cómo crear el tuyo propio Promise objeto que envuelve una función que acepta una devolución de llamada sin el uso de bibliotecas externas.

Con esto, una gran cantidad de código JavaScript heredado se puede mezclar fácilmente con prácticas y bases de código más modernas. Como siempre, el código fuente está disponible en GitHub.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad