Cómo escribir su propia función Promisify desde cero

Introducción

En este artículo, aprenderá a escribir su propia función promisificar desde cero.

La promisificación ayuda a lidiar con las API basadas en devoluciones de llamada mientras mantiene el código consistente con las promesas.

Podríamos simplemente envolver cualquier función new Promise()y no preocuparnos por eso en absoluto. Pero hacer eso cuando tenemos muchas funciones sería redundante.

Si comprende las promesas y las devoluciones de llamada, entonces aprender a escribir funciones de promesa debería ser fácil. Entonces empecemos.

Pero, ¿alguna vez te has preguntado cómo funciona promisificar?

Lo importante es no dejar de cuestionar. La curiosidad tiene su propia razón de ser.

- Albert Einstein

Las promesas se introdujeron en el estándar ECMA-262, sexta edición (ES6) que se publicó en junio de 2015.

Fue una gran mejora con respecto a las devoluciones de llamada, ya que todos sabemos lo ilegible que puede ser el "infierno de devolución de llamada" :)

Como desarrollador de Node.js, debes saber qué es una promesa y cómo funciona internamente, lo que también te ayudará en las entrevistas de JS. No dude en revisarlos rápidamente antes de seguir leyendo.

¿Por qué necesitamos convertir las devoluciones de llamada en promesas?

  1. Con las devoluciones de llamada, si desea hacer algo de forma secuencial, tendrá que especificar un errargumento en cada devolución de llamada, lo cual es redundante. En promises o async-await, puede simplemente agregar un .catchmétodo o bloque que detectará cualquier error que haya ocurrido en la cadena de promesa.
  2. Con las devoluciones de llamada, no tiene control sobre cuándo se llama, en qué contexto o cuántas veces se llama, lo que puede provocar pérdidas de memoria.
  3. Usando promesas, controlamos estos factores (especialmente el manejo de errores) para que el código sea más legible y fácil de mantener.

Cómo hacer que las funciones basadas en callback devuelvan una promesa

Hay dos maneras de hacerlo:

  1. Envuelva la función en otra función que devuelva una promesa. Luego resuelve o rechaza basándose en argumentos de devolución de llamada.
  2. Promisificación: creamos una función util / helper promisifyque transformará todas las API basadas en la primera devolución de llamada de error.

Ejemplo: hay una API basada en devolución de llamada que proporciona la suma de dos números. Queremos prometerlo para que devuelva una thenablepromesa.

const getSumAsync = (num1, num2, callback) => { if (!num1 || !num2) { return callback(new Error("Missing arguments"), null); } return callback(null, num1 + num2); } getSumAsync(1, 1, (err, result) => { if (err){ doSomethingWithError(err) }else { console.log(result) // 2 } })

Envolver en una promesa

Como puede ver, getSumPromisedelega todo el trabajo a la función original getSumAsync, proporcionando su propia devolución de llamada que se traduce en promesa resolve/reject.

Prometer

Cuando necesitamos prometer muchas funciones, podemos crear una función auxiliar promisify.

¿Qué es la Promisificación?

Promisificación significa transformación. Es una conversión de una función que acepta una devolución de llamada en una función que devuelve una promesa.

Usando Node.js util.promisify():

const { promisify } = require('util') const getSumPromise = promisify(getSumAsync) // step 1 getSumPromise(1, 1) // step 2 .then(result => { console.log(result) }) .catch(err =>{ doSomethingWithError(err); })

Entonces parece una función mágica que se está transformando getSumAsyncen lo getSumPromiseque tiene .theny .catchmétodos

Escribamos nuestra propia función promisificar:

Si observa el paso 1 en el código anterior, la promisifyfunción acepta una función como argumento, así que lo primero que tenemos que hacer es escribir una función que pueda hacer lo mismo:

const getSumPromise = myPromisify(getSumAsync) const myPromisify = (fn) => {}

Después de eso, getSumPromise(1, 1)es una llamada de función. Esto significa que nuestro promisify debería devolver otra función que se puede llamar con los mismos argumentos de la función original:

const myPromisify = (fn) => { return (...args) => { } }

En el código anterior, puede ver que estamos difundiendo argumentos porque no sabemos cuántos argumentos tiene la función original. argsserá una matriz que contiene todos los argumentos.

Cuando llamas getSumPromise(1, 1), en realidad estás llamando (...args)=> {}. En la implementación anterior, devuelve una promesa. Es por eso que puedes usar getSumPromise(1, 1).then(..).catch(..).

Espero que hayas entendido que la función contenedora (...args) => {}debería devolver una promesa.

Devolver una promesa

const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { }) } }

Ahora la parte complicada es cómo decidir cuándo hacer resolve or rejectuna promesa.

En realidad, eso lo decidirá la getSumAsyncimplementación de la función original : llamará a la función de devolución de llamada original y solo tenemos que definirla. Entonces basándonos en erry resultlo haremos rejecto   resolvela promesa.

const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { function customCallback(err, result) { if (err) { reject(err) }else { resolve(result); } } }) } }

Nuestro args[]solo consta de argumentos pasados getSumPromise(1, 1)excepto la función de devolución de llamada. Por lo tanto, debe agregar customCallback(err, result)a lo args[]que getSumAsyncllamará la función original en consecuencia, ya que estamos rastreando el resultado customCallback.

Empuje customCallback a args []

const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { function customCallback(err, result) { if (err) { reject(err) }else { resolve(result); } } args.push(customCallback) fn.call(this, ...args) }) } }

Como puede ver, hemos agregado fn.call(this, args), que llamará a la función original en el mismo contexto con los argumentos getSumAsync(1, 1, customCallback). Entonces nuestra función promisificar debería poder hacerlo en resolve/rejectconsecuencia.

The above implementation will work when the original function expects a callback with two arguments, (err, result). That’s what we encounter most often. Then our custom callback is in exactly the right format and promisify works great for such a case.

But what if the original fn expects a callback with more arguments likecallback(err, result1, result2, ...)?

In order to make it compatible with that, we need to modify our myPromisify function which will be an advanced version.

const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { function customCallback(err, ...results) { if (err) { return reject(err) } return resolve(results.length === 1 ? results[0] : results) } args.push(customCallback) fn.call(this, ...args) }) } }

Example:

const getSumAsync = (num1, num2, callback) => { if (!num1 || !num2) { return callback(new Error("Missing dependencies"), null); } const sum = num1 + num2; const message = `Sum is ${sum}` return callback(null, sum, message); } const getSumPromise = myPromisify(getSumAsync) getSumPromise(2, 3).then(arrayOfResults) // [6, 'Sum is 6']

That’s all! Thank you for making it this far!

I hope you’re able to grasp the concept. Try to re-read it again. It’s a bit of code to wrap your head around, but not too complex. Let me know if it was helpful ?

Don’t forget to share it with your friends who are starting with Node.js or need to level up their Node.js skills.

References:

//nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original

//github.com/digitaldesignlabs/es6-promisify

You can read other articles like this at 101node.io.