Funciones asíncronas

Este artículo está dirigido a personas que empiezan con codificación asíncrona en JavaScript para que podamos mantener las cosas simples evitando palabras grandes, funciones de flecha, plantillas literales, etc.

Callbacks es uno de los conceptos más usados de javascript funcional moderno y si alguna vez has usado jQuery, es probable que ya hayas usado callbacks sin siquiera saberlo.

¿Qué cosa son las funciones de callbacks?

Una función de callback en sus términos más simples, es una función que se pasa a otra función, como parámetro. La función de callback se ejecuta dentro de la función donde se pasa y el resultado final se devuelve a la persona que hace el llamado.

Simple, ¿verdad? Ahora vamos a implementar una función de callback para obtener resultados de nivelación en un juego imaginario.

// levelOne() se llama una función de alto orden porque
// acepta otra función como parámetro.
function levelOne(value, callback) {
var newScore = value + 5;
callback(newScore);
}

// Ten en cuenta que no es obligatorio hacer referencia a la función de callback (línea #3) como callback, sino que se denomina así sólo para una mejor comprensión.

function startGame() {
var currentScore = 5;
console.log('Game Started! Current score is ' +  currentScore);
// Aquí el segundo parámetro que estamos pasando a levelOne es la función callback, es decir, una función que se pasa como parámetro.
levelOne(currentScore, function (levelOneReturnedValue) {
console.log('Level One reached! New score is ' +  levelOneReturnedValue);
});
}

startGame();

Una vez dentro de la función startGame(), llamamos a la función levelOne() con parámetros como currentScore y nuestra función callback().

Cuando llamamos levelOne() dentro del ámbito de la función startGame(), de forma asíncrona, JavaScript ejecuta la función levelOne() y el hilo principal sigue adelante con la parte restante de nuestro código.

Esto significa que podemos hacer todo tipo de operaciones como obtener datos de una API, hacer cálculos matemáticos, etc., todo lo cual puede consumir mucho tiempo y, por lo tanto, no estaremos bloqueando nuestro hilo principal para ello. Una vez que la function(levelOne()) ha hecho con sus operaciones, puede ejecutar la función callback que pasamos anteriormente.

Esta es una característica muy útil de la programación de funciones, ya que las callbacks nos permiten manejar el código asincrónicamente sin tener que esperar una respuesta. Por ejemplo, puede hacer una llamada de ajax a un servidor lento con una función de callback y olvidarse completamente de ello y continuar con el código restante. Una vez que se resuelve esa llamada de ajax, la función de devolución de llamada se ejecuta automáticamente.

Pero las Callbacks pueden volverse desagradable si hay múltiples niveles de callbacks a ser ejecutados en una cadena. Tomemos el ejemplo anterior y agreguemos algunos niveles más a nuestro juego.


function levelOne(value, callback) {
var newScore = value + 5;
callback(newScore);
}

function levelTwo(value, callback) {
var newScore = value + 10;
callback(newScore);
}

function levelThree(value, callback) {
var newScore = value + 30;
callback(newScore);
}

// Ten en cuenta que no es necesario hacer referencia a la función de callback como callback cuando llamamos levelOne(), levelTwo() o levelTree(), ya que puede denominarse cualquier cosa.

function startGame() {
var currentScore = 5;
console.log('Comenzó el juego! La puntuación actual es ' + currentScore);
levelOne(currentScore, function (levelOneReturnedValue) {
console.log('¡Nivel Uno alcanzado! El nuevo puntaje es ' + levelOneReturnedValue);
levelTwo(levelOneReturnedValue, function (levelTwoReturnedValue) {
console.log(¡Nivel Dos alcanzado! El nuevo puntaje es ' + levelTwoReturnedValue);
levelThree(levelTwoReturnedValue, function (levelThreeReturnedValue) {
console.log('¡Nivel Tres alcanzado! El nuevo puntaje es ' + levelThreeReturnedValue);
});
});
});

}

startGame();

Espera, ¿qué acaba de pasar? Hemos añadido dos nuevas funciones para la lógica de nivel, levelTwo() y levelThree(). Dentro de levelOne’s callback(línea #22), llamada función levelTwo() con una función de callback y el resultado de callback de levelOne. Y se repite lo mismo para la función de nivel tres.

Ahora sólo imagina en qué se convertirá este código si tuviéramos que implementar la misma lógica para otros 10 niveles. ¿Ya estás entrando en pánico? ¡Bueno, yo sí! A medida que aumenta el número de funciones de callback anidadas, se hace más difícil leer el código e incluso más difícil de depurar.

Esto a menudo se conoce cariñosamente como un infierno de callback. ¿Hay alguna forma de salir de este infierno de callbacks?

Te prometo que hay una forma mejor

Javascript empezó a apoyar Promises from ES6. Las promesas son básicamente objetos que representan la finalización (o el fracaso) de una operación asíncrona, y su valor resultante.

Tratemos de reescribir nuestro ejemplo de callback infiernal con promesas ahora.


function levelOne(value) {
var promise, newScore = value + 5;
return promise = new Promise(function(resolve) {
resolve(newScore);
});
}

function levelTwo(value) {
var promise, newScore = value + 10;
return promise = new Promise(function(resolve) {
resolve(newScore);
});
}

function levelThree(value) {
var promise, newScore = value + 30;
return promise = new Promise(function(resolve) {
resolve(newScore);
});
}

var startGame = new Promise(function (resolve, reject) {
var currentScore = 5;
console.log('Comenzó el juego! La puntuación actual es ' + currentScore);
resolve(currentScore);
});

// La respuesta desde startGame se pasa automáticamente a la función dentro de la subsiguiente ventana de diálogo.
startGame.then(levelOne)
.then(function (result) {
// el valor del resultado es la promesa devuelta de la función levelOne
console.log('Has llegado al Nivel Uno! El nuevo puntaje es ' + result);
return result;
})
.then(levelTwo).then(function (result) {
console.log('Has llegado al Nivel Dos! El nuevo puntaje es ' + result);
return result;
})
.then(levelThree).then(function (result) {
console.log('Has llegado al Nivel Tres! El nuevo puntaje es ' + result);
});

Hemos reescrito nuestras funciones de nivel (Uno/Dos/Tres) para eliminar las callbacks del parámetro de funciones y en lugar de llamar a la función de callback dentro de ellas, las hemos reemplazado con promesas.

Una vez que se resuelve startGame, podemos simplemente llamar a un método .then() en él y manejar el resultado. Podemos encadenar múltiples promesas una tras otra con encadenamiento .then() .

Esto hace que todo el código sea mucho más legible y más fácil de entender en términos de lo que está sucediendo, y luego de lo que sucede a continuación y así sucesivamente.

La razón profunda por la que las promesas son a menudo mejores es que son más componibles, lo que significa que combinar múltiples promesas «sólo funciona», mientras que combinar múltiples callbacks a menudo no lo hace.

Además, cuando tenemos una sola callback contra una sola promesa, es cierto que no hay una diferencia significativa. Es cuando tienes un millón de callbacks contra un millón de promesas que el código basado en promesas tiende a verse mucho mejor.

De acuerdo, hemos escapado con éxito del infierno de la callback y hemos hecho nuestro código mucho más legible con promesas. Pero, ¿y si te dijera que hay una manera de hacerlo más limpio y legible?

(a)Wait for it

Async-wait está soportado en javascript desde ECMA2017. Permiten escribir código basado en promesas como si fuera código sincrónico, pero sin bloquear el hilo principal. Hacen que su código asíncrono sea menos «inteligente» y más legible.

Para ser honesto, las asincrasias no son más que azúcar sintáctico por encima de las promesas, pero hacen que el código asincrónico parezca y se comporte un poco más como código sincrónico, ahí es precisamente donde reside su poder.

Si utiliza la palabra clave Async antes de la definición de una función, puede utilizar la opción de espera dentro de la función. Cuando usted espera una promesa, la función se detiene de una manera que no bloquee hasta que se cumpla la promesa. Si la promesa se cumple, se recupera el valor. Si la promesa es rechazada, el valor rechazado es lanzado.

Veamos ahora cómo se ve nuestra lógica de juego una vez que la reescribamos con async-awaits.


function levelOne(value) {
var promise, newScore = value + 5;
return promise = new Promise(function(resolve) {
resolve(newScore);
});
}

function levelTwo(value) {
var promise, newScore = value + 10;
return promise = new Promise(function(resolve) {
resolve(newScore);
});
}

function levelThree(value) {
var promise, newScore = value + 30;
return promise = new Promise(function(resolve) {
resolve(newScore);
});
}

// la palabra clave async le dice al motor javascript que cualquier función dentro de esta función que tenga la palabra clave en await, debe ser tratada como código asíncrono y debe continuar ejecutándose sólo una vez que esa función se resuelva o falle.


async function startGame() {
var currentScore = 5;
console.log('Comenzó el juego! La puntuación actual es ' + currentScore);
currentScore = await levelOne(currentScore);
console.log('Has llegado al Nivel Uno! El nuevo puntaje es ' + currentScore);
currentScore = await levelTwo(currentScore);
console.log('Has llegado al Nivel Dos! El nuevo puntaje es ' + currentScore);
currentScore = await levelThree(currentScore);
console.log(Has llegado al Nivel Tres! El nuevo puntaje es ' + currentScore);
}

startGame();

Inmediatamente nuestro código se vuelve mucho más legible, pero hay más en Async-await.

La gestión de errores es una de las principales características de Async-await que destaca. Finalmente podemos manejar tanto errores síncronos como asíncronos con la misma construcción con try and catches, lo cual fue una molestia con promesas sin duplicar los bloques de try-catch.

La siguiente mejor mejora del mundo de las buenas y viejas promesas es la depuración de código. Cuando escribimos promesas basadas en funciones de flecha, no podemos establecer puntos de interrupción dentro de nuestras funciones de flecha, por lo que la depuración es difícil a veces. Pero con async-awaits, la depuración es como si se tratara de un fragmento de código sincrónico