Cómo envolver una función JavaScript asíncrona con una promesa

Cómo escribir una función basada en devolución de llamada y luego convertirla en una función basada en Promesa a la que se puede llamar usando async/await.

Para este tutorial, vamos a utilizar el marco JavaScript de pila completa de CheatCode, Joystick. Joystick reúne un marco de interfaz de usuario de front-end con un back-end de Node.js para crear aplicaciones.

Para comenzar, querremos instalar Joystick a través de NPM. Asegúrese de estar usando Node.js 16+ antes de instalar para garantizar la compatibilidad (lea este tutorial primero si necesita aprender a instalar Node.js o ejecutar varias versiones en su computadora):

Terminal

npm i -g @joystick.js/cli

Esto instalará Joystick globalmente en su computadora. Una vez instalado, vamos a crear un nuevo proyecto:

Terminal

joystick create app

Después de unos segundos, verá un mensaje desconectado en cd en su nuevo proyecto y ejecute joystick start :

Terminal

cd app && joystick start

Después de esto, su aplicación debería estar ejecutándose y estamos listos para comenzar.

Escribir una función de ejemplo basada en devolución de llamada

Para comenzar, vamos a escribir una función que usa el patrón de función de devolución de llamada tradicional (me atrevo a decir "de la vieja escuela") que era popular antes de que llegaran las Promesas de JavaScript. En el proyecto que se acaba de crear para usted cuando ejecutó joystick create app arriba, en el /lib carpeta, queremos agregar un nuevo archivo sayHello.js :

/lib/sayHello.js

const sayHello = (name = '', options = {}, callback = null) => {
  setTimeout(() => {
    const greeting = `Hello, ${name}!`;
    callback(null, greeting);
  }, options?.delay);
};

export default sayHello;

Arriba, estamos escribiendo una función de ejemplo llamada sayHello que usa un patrón de devolución de llamada para devolver una respuesta cuando se llama. La razón por la que se puede usar una devolución de llamada es porque la función a la que llamamos necesita hacer algún trabajo y luego responder más tarde. Usando una devolución de llamada, podemos evitar que esa función bloquee JavaScript para que no procese llamadas adicionales en su pila de llamadas mientras esperamos esa respuesta.

Aquí, estamos simulando esa respuesta retrasada llamando a setTimeout() en el cuerpo de nuestra función. Ese setTimeout el retraso de está dictado por las opciones que pasamos a sayHello() cuando lo llamamos. Después de que haya pasado ese retraso y la función de devolución de llamada del tiempo de espera (aquí, la función de flecha se pasa a setTimeout() ) se llama, tomamos el name pasado a sayHello() y concatenarlo en una cadena con Hello, <name> ! .

Una vez que greeting está definido, llamamos al callback() función pasada como argumento final a sayHello pasando null para el primer argumento (donde el consumidor de la función esperaría que se pasara un error, un "estándar" no documentado entre los desarrolladores de JavaScript) y nuestro greeting por el segundo.

Esto es todo lo que necesitamos para nuestro ejemplo. Hagamos un mejor sentido de cómo funciona esto poniéndolo en uso y luego pasemos a convertir sayHello() estar basado en promesas.

Llamar a la función de ejemplo basada en devolución de llamada

Ahora, vamos a abrir un archivo que ya fue creado para nosotros cuando ejecutamos joystick create app arriba:/ui/pages/index/index.js .

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

Cuando abre ese archivo, queremos reemplazar el contenido existente con el fragmento de código anterior. Esto nos dará un nuevo componente Joystick con el que trabajar para probar sayHello() .

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import sayHello from '../../../lib/sayHello';

const Index = ui.component({
  events: {
    'click button': async (event, component) => {
      sayHello('Ryan', { delay: 3000 }, (error, response) => {
        if (error) {
          console.warn(error);
        } else {
          console.log(response);
        }
      });
    },
  },
  render: () => {
    return `
      <div>
        <button>Say Hello</button>
      </div>
    `;
  },
});

export default Index;

Ampliando esto, hemos hecho dos cosas:

  1. En la cadena HTML devuelta por el render() en la parte inferior del componente, hemos agregado un <button></button> etiqueta entre el <div></div> existente etiquetas en las que podemos hacer clic para activar nuestra función.
  2. Para controlar el disparo, justo encima de render() , agregamos un events objeto y definir un detector de eventos para un click evento en nuestro button etiqueta.

A esa definición de detector de eventos 'click button' asignamos una función que se llamará cuando se detecte el evento de clic en el botón. En el interior, llamamos a nuestro sayHello() función que hemos importado arriba. Al llamar a esa función, pasamos los tres argumentos que anticipamos al escribir la función:name como una cadena, un objeto de options con un delay propiedad, y un callback función para llamar cuando nuestro "trabajo" esté hecho.

Aquí, queremos que nuestra función diga Hello, Ryan! después de un retraso de tres segundos. Suponiendo que todo funcione, porque estamos usando console.log() para registrar el response a sayHello en nuestra función de devolución de llamada (esperamos que este sea nuestro greeting cadena), después de 3 segundos, deberíamos ver Hello, Ryan! impreso en la consola.

Si bien esto funciona, no es lo ideal, ya que en algunos contextos (p. ej., tener que esperar varias funciones asíncronas/basadas en devolución de llamada al mismo tiempo), corremos el riesgo de crear lo que se conoce como "infierno de devolución de llamada" o devoluciones de llamada infinitamente anidadas para esperar a que se complete cada llamada.

Afortunadamente, para evitar eso, se introdujeron las promesas de JavaScript en el lenguaje y, junto con ellas, el async/await patrón. Ahora, vamos a tomar el sayHello() envuélvala en una Promesa y luego vea cómo puede limpiar nuestro código en el momento de la llamada.

Envolviendo la función basada en devolución de llamada en una Promesa

Para escribir nuestra versión envuelta en Promesa de sayHello , vamos a confiar en el methods característica de los componentes de Joystick. Si bien esto no es necesario para que esto funcione (puede escribir la función que estamos a punto de escribir en un archivo separado similar a cómo escribimos /lib/sayHello.js ), mantendrá todo en contexto y será más fácil de entender.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import sayHello from '../../../lib/sayHello';

const Index = ui.component({
  methods: {
    sayHello: (name = '', options = {}) => {
      return new Promise((resolve, reject) => {
        sayHello(name, options, (error, response) => {
          if (error) {
            reject(error);
          } else {
            resolve(response);
          }
        });
      }); 
    }
  },
  events: {
    'click button': async (event, component) => {
      const greeting = await component.methods.sayHello('Ryan', { delay: 3000 });
      console.log(greeting);
      // sayHello('Ryan', { delay: 3000 }, (error, response) => {
      //   if (error) {
      //     console.warn(error);
      //   } else {
      //     console.log(response);
      //   }
      // });
    },
  },
  render: () => {
    return `
      <div>
        <button>Do the Thing</button>
      </div>
    `;
  },
});

export default Index;

Aquí, hemos agregado otra propiedad al objeto de opciones pasado a nuestro ui.component() función llamada methods . El objeto asignado aquí nos permite definir funciones misceláneas a las que se puede acceder en cualquier otro lugar de nuestro componente.

Aquí, hemos definido un método sayHello (no debe confundirse con el importado sayHello arriba) que toma dos argumentos:name y options .

Dentro del cuerpo de la función, return una llamada al new Promise() para definir una nueva promesa de JavaScript y para que , pasamos una función que recibe sus propios dos argumentos:resolve y reject . En el interior, las cosas deberían comenzar a parecer familiares. Aquí, llamamos al sayHello , transmitiendo el name y options pasado a nuestro sayHello método .

La idea aquí es que nuestro método funcionará como un "proxy" o control remoto para nuestro sayHello original. función. La diferencia es que para la función de devolución de llamada, observe que tomamos el posible error y response de sayHello , y en lugar de registrarlos en la consola, los pasamos a reject() si hay un error, o resolve() si recibimos una respuesta satisfactoria (nuestro greeting cadena).

Retrocede en nuestro click button handler, podemos ver que esto se está poniendo en uso. Hemos comentado la versión basada en devolución de llamada de sayHello para que podamos ver la diferencia.

Delante de la función pasada a click button , hemos añadido async para indicar a JavaScript que nuestro código usará el await palabra clave en algún lugar dentro de la función que se pasa a click button . Si observamos nuestro refactor, estamos haciendo exactamente eso. Aquí, desde el component instancia pasada automáticamente como segundo argumento a nuestra función de controlador de eventos, llamamos a component.methods.sayHello() pasando el name cadena y options objeto que queremos transmitir al sayHello original función.

Frente a él, colocamos un await palabra clave para decirle a JavaScript que espere la Promesa devuelta por nuestro sayHello en el componente a resolver. Cuando lo hace, esperamos el greeting cadena a pasar a resolve() que se almacenará en el const greeting variable aquí (en este ejemplo, tres segundos después de llamar al método).

Finalmente, una vez que obtengamos un resultado, console.log(greeting) . Lo bueno de esto es que no solo hemos simplificado nuestro código, sino que lo hemos simplificado lo suficiente para que podamos llamarlo junto con otras Promesas sin tener que anidar un montón de devoluciones de llamada.

Terminando

En este tutorial, aprendimos cómo tomar una función asíncrona basada en devolución de llamada existente y envolverla con una Promesa de JavaScript para hacer que la llamada use menos código y funcione bien con otro código asíncrono basado en Promise. Aprendimos a definir la función original basada en la devolución de llamada y a ponerla en práctica discutiendo sus desventajas y, finalmente, aprendimos a usar el methods de Joystick. para ayudarnos a definir nuestra función contenedora basada en Promise.